diff --git a/cmd/kubefwd/kubefwd.go b/cmd/kubefwd/kubefwd.go index a380f26a..67851e80 100644 --- a/cmd/kubefwd/kubefwd.go +++ b/cmd/kubefwd/kubefwd.go @@ -48,7 +48,8 @@ func newRootCmd() *cobra.Command { " kubefwd svc -n default -l \"app in (ws, api)\"\n" + " kubefwd svc -n default -n the-project\n" + " kubefwd svc -n the-project -m 80:8080 -m 443:1443\n" + - " kubefwd svc -n the-project -s path/to/conf.yml\n" + + " kubefwd svc -n the-project -z path/to/conf.yml\n" + + " kubefwd svc -n the-project -r ctx.ns.svc:127.3.3.1\n" + " kubefwd svc --all-namespaces", Long: globalUsage, diff --git a/cmd/kubefwd/services/services.go b/cmd/kubefwd/services/services.go index e1a75148..bc5aa210 100644 --- a/cmd/kubefwd/services/services.go +++ b/cmd/kubefwd/services/services.go @@ -56,7 +56,8 @@ var verbose bool var domain string var mappings []string var isAllNs bool -var svcConfigurationPath string +var fwdConfigurationPath string +var fwdReservations []string func init() { // override error output from k8s.io/apimachinery/pkg/util/runtime @@ -74,7 +75,8 @@ func init() { Cmd.Flags().StringVarP(&domain, "domain", "d", "", "Append a pseudo domain name to generated host names.") Cmd.Flags().StringSliceVarP(&mappings, "mapping", "m", []string{}, "Specify a port mapping. Specify multiple mapping by duplicating this argument.") Cmd.Flags().BoolVarP(&isAllNs, "all-namespaces", "A", false, "Enable --all-namespaces option like kubectl.") - Cmd.Flags().StringVarP(&svcConfigurationPath, "fwd-conf", "z", "", "Define a forward configuration map") + Cmd.Flags().StringSliceVarP(&fwdReservations, "reserve", "r", []string{}, "Specify an IP reservation. Specify multiple reservations by duplicating this argument.") + Cmd.Flags().StringVarP(&fwdConfigurationPath, "fwd-conf", "z", "", "Define an IP reservation configuration") } @@ -91,6 +93,7 @@ var Cmd = &cobra.Command{ " kubefwd svc -n the-project -x prod-cluster\n" + " kubefwd svc -n the-project -m 80:8080 -m 443:1443\n" + " kubefwd svc -n the-project -z path/to/conf.yml\n" + + " kubefwd svc -n the-project -r ctx.ns.svc:127.3.3.1\n" + " kubefwd svc --all-namespaces", Run: runCmd, } @@ -425,24 +428,25 @@ func (opts *NamespaceOpts) AddServiceHandler(obj interface{}) { // Define a service to forward svcfwd := &fwdservice.ServiceFWD{ - ClientSet: opts.ClientSet, - Context: opts.Context, - Namespace: opts.Namespace, - Hostfile: opts.HostFile, - ClientConfig: opts.ClientConfig, - RESTClient: opts.RESTClient, - NamespaceN: opts.NamespaceN, - ClusterN: opts.ClusterN, - Domain: opts.Domain, - PodLabelSelector: selector, - NamespaceServiceLock: opts.NamespaceIPLock, - Svc: svc, - Headless: svc.Spec.ClusterIP == "None", - PortForwards: make(map[string]*fwdport.PortForwardOpts), - SyncDebouncer: debounce.New(5 * time.Second), - DoneChannel: make(chan struct{}), - PortMap: opts.ParsePortMap(mappings), - ServiceConfigPath: svcConfigurationPath, + ClientSet: opts.ClientSet, + Context: opts.Context, + Namespace: opts.Namespace, + Hostfile: opts.HostFile, + ClientConfig: opts.ClientConfig, + RESTClient: opts.RESTClient, + NamespaceN: opts.NamespaceN, + ClusterN: opts.ClusterN, + Domain: opts.Domain, + PodLabelSelector: selector, + NamespaceServiceLock: opts.NamespaceIPLock, + Svc: svc, + Headless: svc.Spec.ClusterIP == "None", + PortForwards: make(map[string]*fwdport.PortForwardOpts), + SyncDebouncer: debounce.New(5 * time.Second), + DoneChannel: make(chan struct{}), + PortMap: opts.ParsePortMap(mappings), + ForwardConfigurationPath: fwdConfigurationPath, + ForwardIPReservations: fwdReservations, } // Add the service to the catalog of services being forwarded diff --git a/example.fwdconf.yml b/example.fwdconf.yml index 5d305424..bb4e79eb 100644 --- a/example.fwdconf.yml +++ b/example.fwdconf.yml @@ -1,10 +1,7 @@ baseUnreservedIP: 127.1.27.1 serviceConfigurations: - - # context that will contain this namespace - context: context.name - # namespace expected to hold this service - namespace: foobar - # name of the service you wish to specify the IP for - serviceName: service-name + - # identifier consisting of context name, namespace, + # and service name separated by a period + identifier: context-name.namespace.service-name # ip address you wish to utilize for this service ip: 127.1.28.1 diff --git a/pkg/fwdIp/fwdIp.go b/pkg/fwdIp/fwdIp.go index e6422488..cc280d20 100644 --- a/pkg/fwdIp/fwdIp.go +++ b/pkg/fwdIp/fwdIp.go @@ -21,6 +21,7 @@ type ForwardIPOpts struct { Namespace string Port string ForwardConfigurationPath string + ForwardIPReservations []string } // Registry is a structure to create and hold all of the @@ -33,15 +34,13 @@ type Registry struct { } type ForwardConfiguration struct { - BaseUnreservedIP string `yaml:"baseUnreservedIP"` - ServiceConfigurations []ServiceConfiguration `yaml:"serviceConfigurations"` + BaseUnreservedIP string `yaml:"baseUnreservedIP"` + ServiceConfigurations []*ServiceConfiguration `yaml:"serviceConfigurations"` } type ServiceConfiguration struct { - Context string `yaml:"context"` - Namespace string `yaml:"namespace"` - ServiceName string `yaml:"serviceName"` - IP string `yaml:"ip"` + Identifier string `yaml:"identifier"` + IP string `yaml:"ip"` } var ipRegistry *Registry @@ -73,7 +72,7 @@ func GetIp(opts ForwardIPOpts) (net.IP, error) { } func determineIP(regKey string, opts ForwardIPOpts) net.IP { - baseUnreservedIP := getBaseUnreservedIP(opts.ForwardConfigurationPath) + baseUnreservedIP := getBaseUnreservedIP(opts) // if a configuration exists use it svcConf := getConfigurationForService(opts) @@ -149,8 +148,8 @@ func ipFromString(ipStr string) (net.IP, error) { return net.IP{byte(octet0), byte(octet1), byte(octet2), byte(octet3)}.To4(), nil } -func getBaseUnreservedIP(forwardConfigurationPath string) []byte { - fwdCfg := getForwardConfiguration(forwardConfigurationPath) +func getBaseUnreservedIP(opts ForwardIPOpts) []byte { + fwdCfg := getForwardConfiguration(opts) ip, err := ipFromString(fwdCfg.BaseUnreservedIP) if err != nil { panic(err) @@ -159,49 +158,75 @@ func getBaseUnreservedIP(forwardConfigurationPath string) []byte { } func getConfigurationForService(opts ForwardIPOpts) *ServiceConfiguration { - fwdCfg := getForwardConfiguration(opts.ForwardConfigurationPath) + fwdCfg := getForwardConfiguration(opts) for _, c := range fwdCfg.ServiceConfigurations { - if c.ServiceName == opts.ServiceName && - c.Namespace == opts.Namespace && - c.Context == opts.Context { - return &c + toMatch := fmt.Sprintf("%s.%s.%s", opts.Context, opts.Namespace, opts.ServiceName) + if c.Identifier == toMatch { + return c } } return nil } -func getForwardConfiguration(forwardConfigurationPath string) *ForwardConfiguration { +func applyCLIPassedReservations(opts ForwardIPOpts, f *ForwardConfiguration) *ForwardConfiguration { + for _, resStr := range opts.ForwardIPReservations { + parts := strings.Split(resStr, ":") + if len(parts) != 2 { + continue // invalid syntax + } + // find any existing + identifier := parts[0] + ipStr := parts[1] + overridden := false + for _, c := range f.ServiceConfigurations { + if c.Identifier == identifier { + c.IP = ipStr + overridden = true + log.Infof("cli reservation flag overriding config for %s now %s", c.Identifier, c.IP) + } + } + if !overridden { + f.ServiceConfigurations = append(f.ServiceConfigurations, &ServiceConfiguration{ + Identifier: identifier, + IP: ipStr, + }) + } + } + return f +} + +func getForwardConfiguration(opts ForwardIPOpts) *ForwardConfiguration { if forwardConfiguration != nil { return forwardConfiguration } - if forwardConfigurationPath == "" { + if opts.ForwardConfigurationPath == "" { forwardConfiguration = defaultConfiguration - return forwardConfiguration + return applyCLIPassedReservations(opts, forwardConfiguration) } - dat, err := os.ReadFile(forwardConfigurationPath) + dat, err := os.ReadFile(opts.ForwardConfigurationPath) if err != nil { // fall back to existing kubefwd base - log.Error(fmt.Sprintf("ForwardConfiguration read error %s", err)) + log.Errorf("ForwardConfiguration read error %s", err) forwardConfiguration = defaultConfiguration - return forwardConfiguration + return applyCLIPassedReservations(opts, forwardConfiguration) } conf := &ForwardConfiguration{} err = yaml.Unmarshal(dat, conf) if err != nil { // fall back to existing kubefwd base - log.Error(fmt.Sprintf("ForwardConfiguration parse error %s", err)) + log.Errorf("ForwardConfiguration parse error %s", err) forwardConfiguration = defaultConfiguration - return forwardConfiguration + return applyCLIPassedReservations(opts, forwardConfiguration) } forwardConfiguration = conf - return forwardConfiguration + return applyCLIPassedReservations(opts, forwardConfiguration) } func (c ServiceConfiguration) String() string { - return fmt.Sprintf("Ctx: %s Ns:%s Svc:%s IP:%s", c.Context, c.Namespace, c.ServiceName, c.IP) + return fmt.Sprintf("ID: %s IP:%s", c.Identifier, c.IP) } diff --git a/pkg/fwdservice/fwdservice.go b/pkg/fwdservice/fwdservice.go index f3acdc2d..3b559585 100644 --- a/pkg/fwdservice/fwdservice.go +++ b/pkg/fwdservice/fwdservice.go @@ -72,7 +72,8 @@ type ServiceFWD struct { PortForwards map[string]*fwdport.PortForwardOpts DoneChannel chan struct{} // After shutdown is complete, this channel will be closed - ServiceConfigPath string + ForwardConfigurationPath string // file path to IP reservation configuration + ForwardIPReservations []string // cli passed IP reservations } /** @@ -243,7 +244,8 @@ func (svcFwd *ServiceFWD) LoopPodsToForward(pods []v1.Pod, includePodNameInHost NamespaceN: svcFwd.NamespaceN, Namespace: svcFwd.Namespace, Port: podPort, - ForwardConfigurationPath: svcFwd.ServiceConfigPath, + ForwardConfigurationPath: svcFwd.ForwardConfigurationPath, + ForwardIPReservations: svcFwd.ForwardIPReservations, } localIp, err := fwdnet.ReadyInterface(opts) if err != nil {