From 9c6da64ab4b2099c96986ebf09e0c4b4ad93e42c Mon Sep 17 00:00:00 2001 From: Adrian Liechti Date: Sat, 6 Jul 2024 22:26:00 +0200 Subject: [PATCH] add grafana, and cluster tunnel --- app/app_port.go | 116 ++++++++++++ app/connect/connect.go | 73 ++++++++ .../cluster_create.go => create/create.go} | 40 ++-- .../cluster_delete.go => delete/delete.go} | 4 +- app/grafana/grafana.go | 32 ++++ .../cluster_setup.go => setup/setup.go} | 8 +- extension/grafana/grafana.go | 177 ++++++++++++++++++ extension/loki/loki.go | 90 +++++++++ .../monitoring.go} | 75 ++++---- extension/tempo/tempo.go | 39 ++++ go.mod | 3 +- go.sum | 2 + main.go | 28 ++- 13 files changed, 624 insertions(+), 63 deletions(-) create mode 100644 app/app_port.go create mode 100644 app/connect/connect.go rename app/{cluster/cluster_create.go => create/create.go} (51%) rename app/{cluster/cluster_delete.go => delete/delete.go} (85%) create mode 100644 app/grafana/grafana.go rename app/{cluster/cluster_setup.go => setup/setup.go} (74%) create mode 100644 extension/grafana/grafana.go create mode 100644 extension/loki/loki.go rename extension/{prometheus/prometheus.go => monitoring/monitoring.go} (55%) create mode 100644 extension/tempo/tempo.go diff --git a/app/app_port.go b/app/app_port.go new file mode 100644 index 0000000..2bf821e --- /dev/null +++ b/app/app_port.go @@ -0,0 +1,116 @@ +package app + +import ( + "errors" + "strconv" + "strings" + + "github.com/adrianliechti/loop/pkg/cli" + "github.com/adrianliechti/loop/pkg/system" +) + +var PortFlag = &cli.IntFlag{ + Name: "port", + Usage: "port", +} + +func Port(c *cli.Context) int { + return c.Int(PortFlag.Name) +} + +func MustPort(c *cli.Context) int { + port := Port(c) + + if port <= 0 { + cli.Fatal(errors.New("port missing")) + } + + return port +} + +var PortsFlag = &cli.StringSliceFlag{ + Name: "port", + Usage: "port mappings", +} + +func Ports(c *cli.Context) (map[int]int, error) { + s := c.StringSlice(PortsFlag.Name) + + result := map[int]int{} + + for _, p := range s { + pair := strings.Split(p, ":") + + if len(pair) > 2 { + return nil, errors.New("invalid port mapping") + } + + if len(pair) == 1 { + pair = []string{pair[0], pair[0]} + } + + source, err := strconv.Atoi(pair[0]) + + if err != nil { + return nil, err + } + + target, err := strconv.Atoi(pair[1]) + + if err != nil { + return nil, err + } + + result[source] = target + } + + return result, nil +} + +func MustPorts(c *cli.Context) map[int]int { + ports, err := Ports(c) + + if err != nil { + cli.Fatal(err) + } + + if len(ports) == 0 { + cli.Fatal(errors.New("ports missing")) + } + + return ports +} + +func PortOrRandom(c *cli.Context, preference int) (int, error) { + port := Port(c) + + if port > 0 { + return port, nil + } + + return system.FreePort(preference) +} + +func MustPortOrRandom(c *cli.Context, preference int) int { + port, err := PortOrRandom(c, preference) + + if err != nil { + cli.Fatal(err) + } + + return port +} + +func RandomPort(c *cli.Context, preference int) (int, error) { + return system.FreePort(preference) +} + +func MustRandomPort(c *cli.Context, preference int) int { + port, err := RandomPort(c, preference) + + if err != nil { + cli.Fatal(err) + } + + return port +} diff --git a/app/connect/connect.go b/app/connect/connect.go new file mode 100644 index 0000000..d53c9ac --- /dev/null +++ b/app/connect/connect.go @@ -0,0 +1,73 @@ +package connect + +import ( + "context" + + "github.com/adrianliechti/devkube/app" + "github.com/adrianliechti/devkube/pkg/cli" + "github.com/adrianliechti/loop/pkg/catapult" + "github.com/adrianliechti/loop/pkg/kubernetes" + "github.com/adrianliechti/loop/pkg/system" +) + +func Command() *cli.Command { + return &cli.Command{ + Name: "connect", + Usage: "forward cluster workload traffic", + + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "namespace", + Usage: "filter namespace(s)", + }, + + &cli.StringFlag{ + Name: "scope", + Usage: "set namespace scope", + }, + }, + + Action: func(c *cli.Context) error { + elevated, err := system.IsElevated() + + if err != nil { + return err + } + + if !elevated { + cli.Fatal("This command must be run as root!") + } + + client := app.MustClient(c) + + var tunnelScope string = "default" + var tunnelNamespaces []string = nil + + if val := c.StringSlice("namespace"); len(val) > 0 { + tunnelScope = val[0] + tunnelNamespaces = val + } + + if val := c.String("scope"); val != "" { + tunnelScope = val + } + + return Catapult(c.Context, client, tunnelNamespaces, tunnelScope) + }, + } +} + +func Catapult(ctx context.Context, client kubernetes.Client, namespaces []string, scope string) error { + catapult, err := catapult.New(client, catapult.CatapultOptions{ + Scope: scope, + Namespaces: namespaces, + + IncludeIngress: true, + }) + + if err != nil { + return err + } + + return catapult.Start(ctx) +} diff --git a/app/cluster/cluster_create.go b/app/create/create.go similarity index 51% rename from app/cluster/cluster_create.go rename to app/create/create.go index 442aa48..1bc004b 100644 --- a/app/cluster/cluster_create.go +++ b/app/create/create.go @@ -1,12 +1,18 @@ -package cluster +package create import ( "github.com/adrianliechti/devkube/app" - "github.com/adrianliechti/devkube/extension/prometheus" + "github.com/adrianliechti/devkube/app/setup" "github.com/adrianliechti/devkube/pkg/cli" + + "github.com/adrianliechti/devkube/extension/certmanager" + "github.com/adrianliechti/devkube/extension/grafana" + "github.com/adrianliechti/devkube/extension/loki" + "github.com/adrianliechti/devkube/extension/monitoring" + "github.com/adrianliechti/devkube/extension/tempo" ) -func CreateCommand() *cli.Command { +func Command() *cli.Command { return &cli.Command{ Name: "create", Usage: "Create cluster", @@ -21,12 +27,24 @@ func CreateCommand() *cli.Command { client := app.MustClient(c) - // if err := certmanager.Ensure(c.Context, client); err != nil { - // //return err - // } + if err := certmanager.Ensure(c.Context, client); err != nil { + return err + } - if err := prometheus.Ensure(c.Context, client); err != nil { - //return err + if err := monitoring.Ensure(c.Context, client); err != nil { + return err + } + + if err := loki.Ensure(c.Context, client); err != nil { + return err + } + + if err := tempo.Ensure(c.Context, client); err != nil { + return err + } + + if err := grafana.Ensure(c.Context, client); err != nil { + return err } // if err := metrics.Install(c.Context, kubeconfig, app.DefaultNamespace); err != nil { @@ -45,11 +63,7 @@ func CreateCommand() *cli.Command { // return err // } - // if err := observability.Install(c.Context, kubeconfig, app.DefaultNamespace); err != nil { - // return err - // } - - return ExportConfig(c.Context, provider, cluster, "") + return setup.Export(c.Context, provider, cluster, "") }, } } diff --git a/app/cluster/cluster_delete.go b/app/delete/delete.go similarity index 85% rename from app/cluster/cluster_delete.go rename to app/delete/delete.go index f50847a..808a566 100644 --- a/app/cluster/cluster_delete.go +++ b/app/delete/delete.go @@ -1,11 +1,11 @@ -package cluster +package delete import ( "github.com/adrianliechti/devkube/app" "github.com/adrianliechti/devkube/pkg/cli" ) -func DeleteCommand() *cli.Command { +func Command() *cli.Command { return &cli.Command{ Name: "delete", Usage: "Delete cluster", diff --git a/app/grafana/grafana.go b/app/grafana/grafana.go new file mode 100644 index 0000000..f61cb3c --- /dev/null +++ b/app/grafana/grafana.go @@ -0,0 +1,32 @@ +package grafana + +import ( + "fmt" + + "github.com/adrianliechti/devkube/app" + "github.com/adrianliechti/devkube/pkg/cli" +) + +func Command() *cli.Command { + return &cli.Command{ + Name: "grafana", + Usage: "Open Grafana", + + Action: func(c *cli.Context) error { + client := app.MustClient(c) + + port := app.MustPortOrRandom(c, 3000) + + ready := make(chan struct{}) + + go func() { + <-ready + + url := fmt.Sprintf("http://127.0.0.1:%d", port) + cli.OpenURL(url) + }() + + return client.ServicePortForward(c.Context, "monitoring", "grafana", "", map[int]int{port: 3000}, ready) + }, + } +} diff --git a/app/cluster/cluster_setup.go b/app/setup/setup.go similarity index 74% rename from app/cluster/cluster_setup.go rename to app/setup/setup.go index ee5c7e0..e7c96c8 100644 --- a/app/cluster/cluster_setup.go +++ b/app/setup/setup.go @@ -1,4 +1,4 @@ -package cluster +package setup import ( "context" @@ -11,7 +11,7 @@ import ( "github.com/adrianliechti/loop/pkg/kubernetes" ) -func SetupCommand() *cli.Command { +func Command() *cli.Command { return &cli.Command{ Name: "setup", Usage: "Setup cluster", @@ -19,12 +19,12 @@ func SetupCommand() *cli.Command { Action: func(c *cli.Context) error { provider, cluster := app.MustCluster(c) - return ExportConfig(c.Context, provider, cluster, "") + return Export(c.Context, provider, cluster, "") }, } } -func ExportConfig(ctx context.Context, provider provider.Provider, cluster string, path string) error { +func Export(ctx context.Context, provider provider.Provider, cluster string, path string) error { if path == "" { path = kubernetes.ConfigPath() } diff --git a/extension/grafana/grafana.go b/extension/grafana/grafana.go new file mode 100644 index 0000000..5ef9cf9 --- /dev/null +++ b/extension/grafana/grafana.go @@ -0,0 +1,177 @@ +package grafana + +import ( + "context" + + "github.com/adrianliechti/devkube/pkg/helm" + "github.com/adrianliechti/loop/pkg/kubernetes" +) + +const ( + name = "grafana" + namespace = "monitoring" + + repoURL = "https://grafana.github.io/helm-charts" + chartName = "grafana" + chartVersion = "8.3.2" +) + +func Ensure(ctx context.Context, client kubernetes.Client) error { + grafanaHost := "" + grafanaURL := "http://" + name + "." + namespace + + values := map[string]any{ + "adminUser": "admin", + "adminPassword": "admin", + + "persistence": map[string]any{ + "enabled": true, + "size": "10Gi", + }, + + "deploymentStrategy": map[string]any{ + "type": "Recreate", + }, + + "sidecar": map[string]any{ + "dashboards": map[string]any{ + "enabled": true, + + "searchNamespace": "ALL", + }, + + "datasources": map[string]any{ + "enabled": true, + + "searchNamespace": "ALL", + }, + + "plugins": map[string]any{ + "enabled": true, + + "searchNamespace": "ALL", + }, + + "notifiers": map[string]any{ + "enabled": true, + + "searchNamespace": "ALL", + }, + }, + + "grafana.ini": map[string]any{ + "server": map[string]any{ + "domain": grafanaHost, + "root_url": grafanaURL, + }, + + "analytics": map[string]any{ + "reporting_enabled": false, + + "check_for_updates": false, + "check_for_plugin_updates": false, + + "feedback_links_enabled": false, + }, + + "users": map[string]any{ + "allow_sign_up": false, + "allow_org_create": false, + "auto_assign_org": true, + "auto_assign_org_role": "Viewer", + "viewers_can_edit": true, + "editors_can_admin": true, + }, + + "auth": map[string]any{ + "disable_login_form": true, + "disable_signout_menu": true, + }, + + "auth.basic": map[string]any{ + "enabled": true, + }, + + "auth.anonymous": map[string]any{ + "enabled": true, + "org_name": "Main Org.", + "org_role": "Viewer", + }, + }, + + "datasources": map[string]any{ + "datasources.yaml": map[string]any{ + "apiVersion": 1, + "datasources": []map[string]any{ + { + "name": "Loki", + "type": "loki", + "uid": "loki", + "url": "http://loki:3100", + "access": "proxy", + }, + { + "name": "Tempo", + "type": "tempo", + "uid": "tempo", + "url": "http://tempo:3100", + "access": "proxy", + + "jsonData": map[string]any{ + "httpMethod": "GET", + + "tracesToLogs": map[string]any{ + "datasourceUid": "loki", + "mapTagNamesEnabled": true, + }, + + "tracesToMetrics": map[string]any{ + "datasourceUid": "prometheus", + }, + + "serviceMap": map[string]any{ + "datasourceUid": "prometheus", + }, + + "nodeGraph": map[string]any{ + "enabled": true, + }, + + "lokiSearch": map[string]any{ + "datasourceUid": "loki", + }, + }, + }, + { + "name": "Prometheus", + "type": "prometheus", + "uid": "prometheus", + "url": "http://monitoring-prometheus:9090", + "access": "proxy", + + "jsonData": map[string]any{ + "httpMethod": "GET", + }, + }, + { + "name": "Alertmanager", + "type": "alertmanager", + "uid": "alertmanager", + "url": "http://monitoring-alertmanager:9093", + "access": "proxy", + + "jsonData": map[string]any{ + "implementation": "prometheus", + }, + }, + }, + }, + }, + } + + if err := helm.Ensure(ctx, client, namespace, name, repoURL, chartName, chartVersion, values); err != nil { + return err + } + + return nil +} diff --git a/extension/loki/loki.go b/extension/loki/loki.go new file mode 100644 index 0000000..646fbea --- /dev/null +++ b/extension/loki/loki.go @@ -0,0 +1,90 @@ +package loki + +import ( + "context" + + "github.com/adrianliechti/devkube/pkg/helm" + "github.com/adrianliechti/loop/pkg/kubernetes" +) + +const ( + name = "loki" + namespace = "monitoring" + + repoURL = "https://grafana.github.io/helm-charts" + chartName = "loki" + chartVersion = "5.48.0" +) + +func Ensure(ctx context.Context, client kubernetes.Client) error { + values := map[string]any{ + "loki": map[string]any{ + "commonConfig": map[string]any{ + "replication_factor": 1, + }, + + "auth_enabled": false, + + "storage": map[string]any{ + "type": "filesystem", + }, + + "querier": map[string]any{ + "max_concurrent": 8, + }, + }, + + "singleBinary": map[string]any{ + "replicas": 1, + + "persistence": map[string]any{ + "size": "10Gi", + }, + }, + + "tableManager": map[string]any{ + "retention_deletes_enabled": true, + "retention_period": "7d", + }, + + "gateway": map[string]any{ + "enabled": false, + }, + + "test": map[string]any{ + "enabled": false, + }, + + "monitoring": map[string]any{ + "dashboards": map[string]any{ + "enabled": false, + }, + + "rules": map[string]any{ + "enabled": false, + }, + + "serviceMonitor": map[string]any{ + "enabled": false, + }, + + "selfMonitoring": map[string]any{ + "enabled": false, + + "grafanaAgent": map[string]any{ + "installOperator": false, + }, + }, + + "lokiCanary": map[string]any{ + "enabled": false, + }, + }, + } + + if err := helm.Ensure(ctx, client, namespace, name, repoURL, chartName, chartVersion, values); err != nil { + return err + } + + return nil +} diff --git a/extension/prometheus/prometheus.go b/extension/monitoring/monitoring.go similarity index 55% rename from extension/prometheus/prometheus.go rename to extension/monitoring/monitoring.go index 209960b..1805fd1 100644 --- a/extension/prometheus/prometheus.go +++ b/extension/monitoring/monitoring.go @@ -1,4 +1,4 @@ -package prometheus +package monitoring import ( "context" @@ -8,17 +8,18 @@ import ( ) const ( + name = "monitoring" namespace = "monitoring" - repo = "https://prometheus-community.github.io/helm-charts" - chart = "kube-prometheus-stack" - version = "61.2.0" + repoURL = "https://prometheus-community.github.io/helm-charts" + chartName = "kube-prometheus-stack" + chartVersion = "61.2.0" ) func Ensure(ctx context.Context, client kubernetes.Client) error { values := map[string]any{ - "nameOverride": "monitoring", - "fullnameOverride": "monitoring", + "nameOverride": name, + "fullnameOverride": name, "cleanPrometheusOperatorObjectNames": true, @@ -52,22 +53,22 @@ func Ensure(ctx context.Context, client kubernetes.Client) error { "forceDeployDatasources": true, }, - // "alertmanager": map[string]any{ - // "alertmanagerSpec": map[string]any{ - // "storage": map[string]any{ - // "volumeClaimTemplate": map[string]any{ - // "spec": map[string]any{ - // "accessModes": []string{"ReadWriteOnce"}, - // "resources": map[string]any{ - // "requests": map[string]any{ - // "storage": "10Gi", - // }, - // }, - // }, - // }, - // }, - // }, - // }, + "alertmanager": map[string]any{ + "alertmanagerSpec": map[string]any{ + "storage": map[string]any{ + "volumeClaimTemplate": map[string]any{ + "spec": map[string]any{ + "accessModes": []string{"ReadWriteOnce"}, + "resources": map[string]any{ + "requests": map[string]any{ + "storage": "10Gi", + }, + }, + }, + }, + }, + }, + }, "prometheus": map[string]any{ "prometheusSpec": map[string]any{ @@ -86,25 +87,25 @@ func Ensure(ctx context.Context, client kubernetes.Client) error { "ruleSelector": nil, "ruleSelectorNilUsesHelmValues": false, - //"retentionSize": "9GiB", - - // "storageSpec": map[string]any{ - // "volumeClaimTemplate": map[string]any{ - // "spec": map[string]any{ - // "accessModes": []string{"ReadWriteOnce"}, - // "resources": map[string]any{ - // "requests": map[string]any{ - // "storage": "10Gi", - // }, - // }, - // }, - // }, - // }, + "retentionSize": "9GiB", + + "storageSpec": map[string]any{ + "volumeClaimTemplate": map[string]any{ + "spec": map[string]any{ + "accessModes": []string{"ReadWriteOnce"}, + "resources": map[string]any{ + "requests": map[string]any{ + "storage": "10Gi", + }, + }, + }, + }, + }, }, }, } - if err := helm.Ensure(ctx, client, namespace, "monitoring", repo, chart, version, values); err != nil { + if err := helm.Ensure(ctx, client, namespace, name, repoURL, chartName, chartVersion, values); err != nil { return err } diff --git a/extension/tempo/tempo.go b/extension/tempo/tempo.go new file mode 100644 index 0000000..28f2f8e --- /dev/null +++ b/extension/tempo/tempo.go @@ -0,0 +1,39 @@ +package tempo + +import ( + "context" + + "github.com/adrianliechti/devkube/pkg/helm" + "github.com/adrianliechti/loop/pkg/kubernetes" +) + +const ( + name = "tempo" + namespace = "monitoring" + + repoURL = "https://grafana.github.io/helm-charts" + chartName = "tempo" + chartVersion = "1.8.0" +) + +func Ensure(ctx context.Context, client kubernetes.Client) error { + values := map[string]any{ + "tempo": map[string]any{ + "metricsGenerator": map[string]any{ + "enabled": true, + "remoteWriteUrl": "http://monitoring-prometheus:9090/api/v1/write", + }, + }, + + "persistence": map[string]any{ + "enabled": true, + "size": "10Gi", + }, + } + + if err := helm.Ensure(ctx, client, namespace, name, repoURL, chartName, chartVersion, values); err != nil { + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 861ba19..bf57b7b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/adrianliechti/loop v0.16.7-0.20240706160559-5aab856e5393 github.com/charmbracelet/huh v0.4.2 github.com/charmbracelet/lipgloss v0.11.0 + github.com/lmittmann/tint v1.0.4 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/urfave/cli/v2 v2.27.2 @@ -149,6 +150,7 @@ require ( go.opentelemetry.io/otel/trace v1.19.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect golang.org/x/sync v0.7.0 // indirect @@ -156,7 +158,6 @@ require ( golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/grpc v1.58.3 // indirect diff --git a/go.sum b/go.sum index eb60fe9..ab638ec 100644 --- a/go.sum +++ b/go.sum @@ -279,6 +279,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= +github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= diff --git a/main.go b/main.go index 6717735..86f5ffc 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,33 @@ package main import ( "context" + "log/slog" "os" "os/signal" + "syscall" + "time" - "github.com/adrianliechti/devkube/app/cluster" - "github.com/adrianliechti/loop/pkg/cli" + "github.com/adrianliechti/devkube/app/connect" + "github.com/adrianliechti/devkube/app/create" + "github.com/adrianliechti/devkube/app/delete" + "github.com/adrianliechti/devkube/app/grafana" + "github.com/adrianliechti/devkube/app/setup" + "github.com/adrianliechti/devkube/pkg/cli" + + "github.com/lmittmann/tint" ) var version string func main() { - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill, syscall.SIGTERM) defer stop() + slog.SetDefault(slog.New(tint.NewHandler(os.Stderr, &tint.Options{ + Level: slog.LevelInfo, + TimeFormat: time.Kitchen, + }))) + app := initApp() if err := app.RunContext(ctx, os.Args); err != nil { @@ -32,10 +46,12 @@ func initApp() cli.App { HideHelpCommand: true, Commands: []*cli.Command{ - cluster.SetupCommand(), + create.Command(), + delete.Command(), - cluster.CreateCommand(), - cluster.DeleteCommand(), + setup.Command(), + connect.Command(), + grafana.Command(), }, } }