diff --git a/cmd/tcl/kubectl-testkube/devbox/command.go b/cmd/tcl/kubectl-testkube/devbox/command.go index 80c19e06e1..d7448aabf3 100644 --- a/cmd/tcl/kubectl-testkube/devbox/command.go +++ b/cmd/tcl/kubectl-testkube/devbox/command.go @@ -122,6 +122,8 @@ func NewDevBoxCommand() *cobra.Command { binaryStoragePod := namespace.Pod("devbox-binary") mongoPod := namespace.Pod("devbox-mongodb") minioPod := namespace.Pod("devbox-minio") + prometheusPod := namespace.Pod("devbox-prometheus") + grafanaPod := namespace.Pod("devbox-grafana") // Initialize binaries interceptorBin := devutils.NewBinary(InterceptorMainPath, cluster.OperatingSystem(), cluster.Architecture()) @@ -143,6 +145,8 @@ func NewDevBoxCommand() *cobra.Command { binaryStorage := devutils.NewBinaryStorage(binaryStoragePod, binaryStorageBin) mongo := devutils.NewMongo(mongoPod) minio := devutils.NewMinio(minioPod) + prometheus := devutils.NewPrometheus(prometheusPod) + grafana := devutils.NewGrafana(grafanaPod) var env *client.Environment // Cleanup @@ -236,6 +240,34 @@ func NewDevBoxCommand() *cobra.Command { return nil }) + // Deploying Prometheus + g.Go(func() error { + fmt.Println("[Prometheus] Deploying...") + if err = prometheus.Create(ctx); err != nil { + fail(errors.Wrap(err, "failed to create prometheus instance")) + } + fmt.Println("[Prometheus] Waiting for readiness...") + if err = prometheus.WaitForReady(ctx); err != nil { + fail(errors.Wrap(err, "failed to create prometheus instance")) + } + fmt.Println("[Prometheus] Ready") + return nil + }) + + // Deploying Grafana + g.Go(func() error { + fmt.Println("[Grafana] Deploying...") + if err = grafana.Create(ctx); err != nil { + fail(errors.Wrap(err, "failed to create grafana instance")) + } + fmt.Println("[Grafana] Waiting for readiness...") + if err = grafana.WaitForReady(ctx); err != nil { + fail(errors.Wrap(err, "failed to create grafana instance")) + } + fmt.Printf("[Grafana] Ready at %s\n", grafana.LocalAddress()) + return nil + }) + // Deploying binary storage g.Go(func() error { fmt.Println("[Binary Storage] Building...") @@ -615,6 +647,11 @@ func NewDevBoxCommand() *cobra.Command { color.Green.Println("Development box is ready. Took", time.Since(startTs).Truncate(time.Millisecond)) fmt.Println("Namespace:", namespace.Name()) + if termlink.SupportsHyperlinks() { + fmt.Println("Grafana:", termlink.Link(grafana.LocalAddress(), grafana.LocalAddress())) + } else { + fmt.Println("Grafana:", grafana.LocalAddress()) + } if !oss { if termlink.SupportsHyperlinks() { fmt.Println("Dashboard:", termlink.Link(cloud.DashboardUrl(env.Slug, "dashboard/test-workflows"), cloud.DashboardUrl(env.Slug, "dashboard/test-workflows"))) diff --git a/cmd/tcl/kubectl-testkube/devbox/devutils/grafana.go b/cmd/tcl/kubectl-testkube/devbox/devutils/grafana.go new file mode 100644 index 0000000000..547101287f --- /dev/null +++ b/cmd/tcl/kubectl-testkube/devbox/devutils/grafana.go @@ -0,0 +1,130 @@ +// Copyright 2024 Testkube. +// +// Licensed as a Testkube Pro file under the Testkube Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt + +package devutils + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/kubeshop/testkube/internal/common" +) + +type Grafana struct { + pod *PodObject + localPort int +} + +func NewGrafana(pod *PodObject) *Grafana { + return &Grafana{ + pod: pod, + } +} + +const ( + grafanaProvisioningPrometheusDataSource = ` +apiVersion: 1 + +deleteDatasources: + - name: Prometheus + orgId: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + orgId: 1 + url: http://devbox-prometheus:9090` +) + +func (r *Grafana) Create(ctx context.Context) error { + _, err := r.pod.ClientSet().CoreV1().ConfigMaps(r.pod.Namespace()).Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "devbox-grafana-provisioning-datasources"}, + Data: map[string]string{ + "prometheus.yml": grafanaProvisioningPrometheusDataSource, + }, + }, metav1.CreateOptions{}) + if err != nil { + return err + } + + err = r.pod.Create(ctx, &corev1.Pod{ + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: common.Ptr(int64(1)), + Volumes: []corev1.Volume{ + {Name: "data", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + {Name: "provisioning-datasources", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "devbox-grafana-provisioning-datasources"}, + }}}, + }, + Containers: []corev1.Container{ + { + Name: "prometheus", + Image: "grafana/grafana-oss:11.3.1", + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + {Name: "GF_USERS_ALLOW_SIGN_UP", Value: "false"}, + {Name: "GF_AUTH_ANONYMOUS_ENABLED", Value: "true"}, + {Name: "GF_AUTH_ANONYMOUS_ORG_ROLE", Value: "Admin"}, + {Name: "GF_AUTH_DISABLE_LOGIN_FORM", Value: "true"}, + }, + VolumeMounts: []corev1.VolumeMount{ + {Name: "data", MountPath: "/var/lib/grafana"}, + {Name: "provisioning-datasources", MountPath: "/etc/grafana/provisioning/datasources"}, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt32(3000), + }, + }, + PeriodSeconds: 1, + }, + }, + }, + }, + }) + if err != nil { + return err + } + + err = r.pod.CreateService(ctx, corev1.ServicePort{ + Name: "web", + Protocol: "TCP", + Port: 3000, + TargetPort: intstr.FromInt32(3000), + }) + if err != nil { + return err + } + + err = r.pod.WaitForReady(ctx) + if err != nil { + return err + } + + r.localPort = GetFreePort() + err = r.pod.Forward(ctx, 3000, r.localPort, true) + if err != nil { + return err + } + + return nil +} + +func (r *Grafana) LocalAddress() string { + return fmt.Sprintf("http://localhost:%d", r.localPort) +} + +func (r *Grafana) WaitForReady(ctx context.Context) error { + return r.pod.WaitForReady(ctx) +} diff --git a/cmd/tcl/kubectl-testkube/devbox/devutils/prometheus.go b/cmd/tcl/kubectl-testkube/devbox/devutils/prometheus.go new file mode 100644 index 0000000000..4ba4fe56b2 --- /dev/null +++ b/cmd/tcl/kubectl-testkube/devbox/devutils/prometheus.go @@ -0,0 +1,98 @@ +// Copyright 2024 Testkube. +// +// Licensed as a Testkube Pro file under the Testkube Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt + +package devutils + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/kubeshop/testkube/internal/common" +) + +type Prometheus struct { + pod *PodObject +} + +func NewPrometheus(pod *PodObject) *Prometheus { + return &Prometheus{ + pod: pod, + } +} + +const ( + prometheusConfig = ` +global: + scrape_interval: 1s + scrape_timeout: 500ms + evaluation_interval: 1s + +scrape_configs: +- job_name: 'Agent' + honor_labels: true + metrics_path: /metrics + static_configs: + - targets: [ 'devbox-agent:8088' ]` +) + +func (r *Prometheus) Create(ctx context.Context) error { + _, err := r.pod.ClientSet().CoreV1().ConfigMaps(r.pod.Namespace()).Create(ctx, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "devbox-prometheus-config"}, + Data: map[string]string{"prometheus.yml": prometheusConfig}, + }, metav1.CreateOptions{}) + if err != nil { + return err + } + + err = r.pod.Create(ctx, &corev1.Pod{ + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: common.Ptr(int64(1)), + Volumes: []corev1.Volume{ + {Name: "data", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + {Name: "config", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "devbox-prometheus-config"}, + }}}, + }, + Containers: []corev1.Container{ + { + Name: "prometheus", + Image: "prom/prometheus:v2.53.3", + ImagePullPolicy: corev1.PullIfNotPresent, + VolumeMounts: []corev1.VolumeMount{ + {Name: "data", MountPath: "/prometheus"}, + {Name: "config", MountPath: "/etc/prometheus/prometheus.yml", SubPath: "prometheus.yml"}, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + TCPSocket: &corev1.TCPSocketAction{ + Port: intstr.FromInt32(9090), + }, + }, + PeriodSeconds: 1, + }, + }, + }, + }, + }) + if err != nil { + return err + } + return r.pod.CreateService(ctx, corev1.ServicePort{ + Name: "api", + Protocol: "TCP", + Port: 9090, + TargetPort: intstr.FromInt32(9090), + }) +} + +func (r *Prometheus) WaitForReady(ctx context.Context) error { + return r.pod.WaitForReady(ctx) +}