|
|
|
@ -18,6 +18,7 @@ import (
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
|
"golang.org/x/exp/slices"
|
|
|
|
|
"golang.org/x/oauth2/clientcredentials"
|
|
|
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
|
|
@ -38,22 +39,29 @@ import (
|
|
|
|
|
"sigs.k8s.io/controller-runtime/pkg/source"
|
|
|
|
|
"sigs.k8s.io/yaml"
|
|
|
|
|
"tailscale.com/client/tailscale"
|
|
|
|
|
"tailscale.com/ipn"
|
|
|
|
|
"tailscale.com/ipn/store/kubestore"
|
|
|
|
|
"tailscale.com/tsnet"
|
|
|
|
|
"tailscale.com/types/logger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
// Required to use our client API. We're fine with the instability since the
|
|
|
|
|
// client lives in the same repo as this code.
|
|
|
|
|
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
hostname = defaultEnv("OPERATOR_HOSTNAME", "tailscale-operator")
|
|
|
|
|
kubeSecret = defaultEnv("OPERATOR_SECRET", "")
|
|
|
|
|
operatorTags = defaultEnv("OPERATOR_INITIAL_TAGS", "tag:k8s-operator")
|
|
|
|
|
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "default")
|
|
|
|
|
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
|
|
|
|
|
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
|
|
|
|
|
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
|
|
|
|
|
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
|
|
|
|
|
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
|
|
|
|
var opts []kzap.Opts
|
|
|
|
|
switch tslogging {
|
|
|
|
|
case "info":
|
|
|
|
@ -66,6 +74,25 @@ func main() {
|
|
|
|
|
zlog := kzap.NewRaw(opts...).Sugar()
|
|
|
|
|
logf.SetLogger(zapr.NewLogger(zlog.Desugar()))
|
|
|
|
|
startlog := zlog.Named("startup")
|
|
|
|
|
|
|
|
|
|
if clientIDPath == "" || clientSecretPath == "" {
|
|
|
|
|
startlog.Fatalf("CLIENT_ID_FILE and CLIENT_SECRET_FILE must be set")
|
|
|
|
|
}
|
|
|
|
|
clientID, err := os.ReadFile(clientIDPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
startlog.Fatalf("reading client ID %q: %v", clientIDPath, err)
|
|
|
|
|
}
|
|
|
|
|
clientSecret, err := os.ReadFile(clientSecretPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
startlog.Fatalf("reading client secret %q: %v", clientSecretPath, err)
|
|
|
|
|
}
|
|
|
|
|
credentials := clientcredentials.Config{
|
|
|
|
|
ClientID: string(clientID),
|
|
|
|
|
ClientSecret: string(clientSecret),
|
|
|
|
|
TokenURL: "https://login.tailscale.com/api/v2/oauth/token",
|
|
|
|
|
}
|
|
|
|
|
tsClient := tailscale.NewClient("-", nil)
|
|
|
|
|
tsClient.HTTPClient = credentials.Client(context.Background())
|
|
|
|
|
s := &tsnet.Server{
|
|
|
|
|
Hostname: hostname,
|
|
|
|
|
Logf: zlog.Named("tailscaled").Debugf,
|
|
|
|
@ -87,10 +114,11 @@ func main() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
loginShown := false
|
|
|
|
|
loginDone := false
|
|
|
|
|
machineAuthShown := false
|
|
|
|
|
waitOnline:
|
|
|
|
|
for {
|
|
|
|
|
startlog.Debugf("querying tailscaled status")
|
|
|
|
|
st, err := lc.StatusWithoutPeers(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
startlog.Fatalf("getting status: %v", err)
|
|
|
|
@ -99,10 +127,32 @@ waitOnline:
|
|
|
|
|
case "Running":
|
|
|
|
|
break waitOnline
|
|
|
|
|
case "NeedsLogin":
|
|
|
|
|
if !loginShown && st.AuthURL != "" {
|
|
|
|
|
startlog.Infof("tailscale needs login, please visit: %s", st.AuthURL)
|
|
|
|
|
loginShown = true
|
|
|
|
|
if loginDone {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
caps := tailscale.KeyCapabilities{
|
|
|
|
|
Devices: tailscale.KeyDeviceCapabilities{
|
|
|
|
|
Create: tailscale.KeyDeviceCreateCapabilities{
|
|
|
|
|
Reusable: false,
|
|
|
|
|
Preauthorized: true,
|
|
|
|
|
Tags: strings.Split(operatorTags, ","),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
authkey, _, err := tsClient.CreateKey(ctx, caps)
|
|
|
|
|
if err != nil {
|
|
|
|
|
startlog.Fatalf("creating operator authkey: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := lc.Start(ctx, ipn.Options{
|
|
|
|
|
AuthKey: authkey,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
startlog.Fatalf("starting tailscale: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := lc.StartLoginInteractive(ctx); err != nil {
|
|
|
|
|
startlog.Fatalf("starting login: %v", err)
|
|
|
|
|
}
|
|
|
|
|
startlog.Debugf("requested login by authkey")
|
|
|
|
|
loginDone = true
|
|
|
|
|
case "NeedsMachineAuth":
|
|
|
|
|
if !machineAuthShown {
|
|
|
|
|
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
|
|
|
|
@ -114,6 +164,14 @@ waitOnline:
|
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sr := &ServiceReconciler{
|
|
|
|
|
tsClient: tsClient,
|
|
|
|
|
defaultTags: strings.Split(tags, ","),
|
|
|
|
|
operatorNamespace: tsNamespace,
|
|
|
|
|
proxyImage: image,
|
|
|
|
|
logger: zlog.Named("service-reconciler"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For secrets and statefulsets, we only get permission to touch the objects
|
|
|
|
|
// in the controller's own namespace. This cannot be expressed by
|
|
|
|
|
// .Watches(...) below, instead you have to add a per-type field selector to
|
|
|
|
@ -134,17 +192,7 @@ waitOnline:
|
|
|
|
|
if err != nil {
|
|
|
|
|
startlog.Fatalf("could not create manager: %v", err)
|
|
|
|
|
}
|
|
|
|
|
tsClient, err := s.APIClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
startlog.Fatalf("getting tailscale client: %v", err)
|
|
|
|
|
}
|
|
|
|
|
sr := &ServiceReconciler{
|
|
|
|
|
tsClient: tsClient,
|
|
|
|
|
defaultTags: strings.Split(tags, ","),
|
|
|
|
|
operatorNamespace: tsNamespace,
|
|
|
|
|
proxyImage: image,
|
|
|
|
|
logger: zlog.Named("service-reconciler"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
|
|
|
|
|
ls := o.GetLabels()
|
|
|
|
|
if ls[LabelManaged] != "true" {
|
|
|
|
|