-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 57b3c9e
Showing
17 changed files
with
1,411 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# MIT LICENSE | ||
|
||
Copyright (C) 2023 Holoplot GmbH | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# kubelish - mDNS service publisher for Kubernetes cluster nodes | ||
|
||
This project implements a way to expose k8s services with external IPs as mDNS services. | ||
|
||
The rationale behind this project is to allow k8s services to be discovered by | ||
devices on the local network dynamically, without the need to configure static IPs | ||
or DNS entries. This is particularly useful for IoT devices, which are often unable | ||
to become part of a traditional service mesh but need to access services of | ||
the k8s cluster. | ||
|
||
In contrast to other solutions, this project exposes mDNS services (SRV records) | ||
rather than hosts (A records) which has the advantage that the daemon can run | ||
on multiple nodes of the same cluster that have IP addresses in the same subnet, | ||
and offer the service on each of them. Clients will hence pick one of the nodes at | ||
random. | ||
|
||
## How it works | ||
|
||
The `kubelish` daemon runs on each node of the cluster and listens for new `LoadBalancer` | ||
services with external IPs. When a service is created or updated, the daemon checks | ||
the annotations of the service to see if it should be exposed. | ||
|
||
The following keys are required: | ||
|
||
```yaml | ||
metadata: | ||
annotations: | ||
kubelish/service-name: Example | ||
kubelish/service-type: _example._tcp | ||
kubelish/txt: Optional TXT record to be exposed along with the service on mDNS | ||
``` | ||
If the service is annotated that way, and one of its external IP addresses is | ||
a local IP address of the node the daemon is running on, the service will be | ||
exposed as an mDNS service. | ||
If a service is deleted or updated to not be exposed, the daemon will remove | ||
the mDNS service. | ||
## Running the daemon | ||
The daemon must be run natively on a node of the cluster, outside of Kubernetes. | ||
Install the daemon by using | ||
```bash | ||
go install github.com/holoplot/kubelish/cmd/kubelish@latest | ||
``` | ||
|
||
Then move the binary to a suitable location in your `$PATH`, e.g. `/usr/local/bin/kubelish`. | ||
|
||
The binary supports the following commands: | ||
|
||
- `kubelish watch` - Runs the watcher daemon | ||
- `kubelish add <k8s-service>` - Adds annotations to a service to expose it as an mDNS service and dumps the YAML to stdout | ||
- `kubelish remove <k8s-service>` - Removes annotations from a service and dumps the YAML to stdout | ||
|
||
A systemd service file is [kubelish.service](provided). Make sure you | ||
edit the file to set the correct path to the binary and the kubeconfig file. | ||
|
||
### Global flags | ||
|
||
The following flags can be used with all commands: | ||
* `--namespace` - Namespace to watch for services. Defaults to `default`. | ||
|
||
### Environment variables | ||
|
||
The environment variables below can be used to configure the daemon: | ||
|
||
- `KUBECONFIG` - Path to the kubeconfig file to use. Defaults to `~/.kube/config`. | ||
|
||
### Example | ||
|
||
Let's assume we have a service called `my-loadbalancer` in the `default` namespace that | ||
you want to expose as an mDNS service. | ||
|
||
First, run the daemon on the node that has the IP address of the service: | ||
|
||
```bash | ||
kubelish watch | ||
``` | ||
|
||
Then, in a second terminal, add the annotations to the service using the following command: | ||
|
||
```bash | ||
kubelish add my-loadbalancer --service-name example --service-type _example._tcp --txt Example \ | ||
| kubectl apply -f - | ||
``` | ||
|
||
The service should now be exposed as an mDNS service. You can verify this by using | ||
the `avahi-browse` command: | ||
|
||
```bash | ||
avahi-browse -r _example._tcp | ||
``` | ||
|
||
To remove the service, run the following command: | ||
|
||
```bash | ||
kubelish remove my-loadbalancer | kubectl apply -f - | ||
``` | ||
|
||
## Limitations | ||
|
||
This project is still in its early stages and has some limitations: | ||
|
||
- Only supports Linux | ||
- Depends on `avahi-daemon` to be installed on the host | ||
- Only services with a single exposed port are supported | ||
- Does not support miniKube's way of exposing services with `minikube tunnel` | ||
|
||
## License | ||
|
||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package cmd | ||
|
||
import ( | ||
"github.com/holoplot/kubelish/pkg/meta" | ||
"github.com/rs/zerolog/log" | ||
"github.com/spf13/cobra" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/tools/clientcmd" | ||
) | ||
|
||
var ( | ||
serviceName string | ||
serviceType string | ||
txt string | ||
) | ||
|
||
func doAdd(cmd *cobra.Command, args []string) { | ||
kubeConfig := getKubeConfig() | ||
|
||
if len(args) != 1 { | ||
log.Fatal().Msg("You must specify a k8s service name") | ||
} | ||
|
||
if serviceName == "" { | ||
log.Fatal().Msg("You must specify an mDNS service name") | ||
} | ||
|
||
if serviceType == "" { | ||
log.Fatal().Msg("You must specify an mDNS service type") | ||
} | ||
|
||
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) | ||
|
||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to build config from flags") | ||
} | ||
|
||
clientSet, err := kubernetes.NewForConfig(config) | ||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to create clientset") | ||
} | ||
|
||
svc, err := clientSet.CoreV1().Services(namespace).Get(cmd.Context(), args[0], metav1.GetOptions{}) | ||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to get service") | ||
} | ||
|
||
an := meta.Annotations{ | ||
ServiceName: serviceName, | ||
ServiceType: serviceType, | ||
Txt: txt, | ||
} | ||
|
||
meta.AddAnnotationsToService(svc, &an) | ||
|
||
dumpServiceYAML(svc) | ||
} | ||
|
||
func addCmd() *cobra.Command { | ||
cmd := &cobra.Command{ | ||
Use: "add <k8s-service>", | ||
Short: "Add annotations to a Kubernetes service", | ||
Long: "Add annotations to a Kubernetes service and print the resulting yaml to stdout", | ||
Run: doAdd, | ||
PreRun: func(cmd *cobra.Command, args []string) { | ||
setupLogger() | ||
}, | ||
} | ||
|
||
cmd.Flags().StringVarP(&serviceName, "service-name", "s", "", "The mDNS service name") | ||
cmd.Flags().StringVarP(&serviceType, "service-type", "t", "", "The mDNS service type") | ||
cmd.Flags().StringVarP(&txt, "txt", "x", "", "The mDNS TXT record") | ||
|
||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package cmd | ||
|
||
import ( | ||
"github.com/holoplot/kubelish/pkg/meta" | ||
"github.com/rs/zerolog/log" | ||
"github.com/spf13/cobra" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/tools/clientcmd" | ||
) | ||
|
||
func doRemove(cmd *cobra.Command, args []string) { | ||
kubeConfig := getKubeConfig() | ||
|
||
if len(args) != 1 { | ||
log.Fatal().Msg("You must specify a k8s service name") | ||
} | ||
|
||
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig) | ||
|
||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to build config from flags") | ||
} | ||
|
||
clientSet, err := kubernetes.NewForConfig(config) | ||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to create clientset") | ||
} | ||
|
||
svc, err := clientSet.CoreV1().Services(namespace).Get(cmd.Context(), args[0], metav1.GetOptions{}) | ||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to get service") | ||
} | ||
|
||
meta.RemoveAnnotationsFromService(svc) | ||
|
||
dumpServiceYAML(svc) | ||
} | ||
|
||
func removeCmd() *cobra.Command { | ||
return &cobra.Command{ | ||
Use: "remove <k8s-service>", | ||
Long: `Remove annotations from a Kubernetes service and print the resulting yaml to stdout`, | ||
Run: func(cmd *cobra.Command, args []string) { | ||
doRemove(cmd, args) | ||
}, | ||
PreRun: func(cmd *cobra.Command, args []string) { | ||
setupLogger() | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package cmd | ||
|
||
import ( | ||
"github.com/rs/zerolog/log" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ( | ||
debug *bool | ||
namespace string | ||
) | ||
|
||
func Execute() { | ||
rootCmd := &cobra.Command{ | ||
Use: "kubelish", | ||
Short: "kubelish is a service discovery tool for Kubernetes", | ||
Long: `kubelish is a service discovery tool for Kubernetes. It publishes Kubernetes services to the local network using mDNS.`, | ||
PersistentPreRun: func(cmd *cobra.Command, args []string) { | ||
setupLogger() | ||
}, | ||
} | ||
|
||
debug = rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug log output") | ||
rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "The namespace to operate on") | ||
|
||
rootCmd.AddCommand(addCmd()) | ||
rootCmd.AddCommand(removeCmd()) | ||
rootCmd.AddCommand(watchCmd()) | ||
|
||
if err := rootCmd.Execute(); err != nil { | ||
log.Fatal().Err(err).Msg("Failed to execute root command") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package cmd | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/rs/zerolog" | ||
"github.com/rs/zerolog/log" | ||
corev1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/util/homedir" | ||
"sigs.k8s.io/yaml" | ||
) | ||
|
||
func setupLogger() { | ||
if *debug { | ||
zerolog.SetGlobalLevel(zerolog.DebugLevel) | ||
} | ||
} | ||
|
||
func getKubeConfig() string { | ||
kubeConfig, ok := os.LookupEnv("KUBECONFIG") | ||
if !ok { | ||
kubeConfig = filepath.Join(homedir.HomeDir(), ".kube", "config") | ||
} | ||
return kubeConfig | ||
} | ||
|
||
func dumpServiceYAML(svc *corev1.Service) { | ||
svc.Kind = "Service" | ||
svc.APIVersion = "v1" | ||
svc.ManagedFields = make([]metav1.ManagedFieldsEntry, 0) | ||
|
||
b, err := yaml.Marshal(svc) | ||
if err != nil { | ||
log.Fatal().Err(err).Msg("Failed to marshal service") | ||
} | ||
|
||
os.Stdout.Write(b) | ||
} |
Oops, something went wrong.