Skip to content

Commit

Permalink
Merge pull request #29 from Cloudzero/cp-21656
Browse files Browse the repository at this point in the history
CP-21656: Dynamically Populate Service Endpoints
  • Loading branch information
bdrennz authored Sep 11, 2024
2 parents e54dd48 + 3398279 commit b2304d9
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 45 deletions.
31 changes: 14 additions & 17 deletions pkg/cmd/config/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,29 @@ func NewCommand(ctx context.Context) *cli.Command {
&cli.StringFlag{Name: config.FlagClusterName, Aliases: []string{"c"}, Usage: config.FlagDescClusterName, Required: true},
&cli.StringFlag{Name: config.FlagRegion, Aliases: []string{"r"}, Usage: config.FlagDescRegion, Required: true},
&cli.StringFlag{Name: config.FlagConfigFile, Aliases: configAlias, Usage: "output configuration file. if omitted output will print to standard out", Required: false},
&cli.StringFlag{Name: "kubeconfig", Usage: "absolute path to the kubeconfig file", Required: false},
},
Action: func(c *cli.Context) error {
kubeconfigPath := c.String("kubeconfig")
clientset, err := k8s.BuildKubeClient(kubeconfigPath)
if err != nil {
return err
}

kubeStateMetricsURL, nodeExporterURL, err := k8s.GetServiceURLs(ctx, clientset)
if err != nil {
return err
}

return Generate(map[string]interface{}{ //nolint: gofmt
"ChartVerson": getCurrentChartVersion(),
"AgentVersion": getCurrentAgentVersion(),
"AccountID": c.String(config.FlagAccountID),
"ClusterName": c.String(config.FlagClusterName),
"Region": c.String(config.FlagRegion),
"CloudzeroHost": build.PlatformEndpoint,
"KubeStateMetricsURL": "http://kube-state-metrics.your-namespace.svc.cluster.local:8080",
"PromNodeExporterURL": "http://node-exporter.your-namespace.svc.cluster.local:9100",
"KubeStateMetricsURL": kubeStateMetricsURL,
"PromNodeExporterURL": nodeExporterURL,
}, c.String(config.FlagConfigFile))
},
},
Expand Down Expand Up @@ -75,21 +87,6 @@ func NewCommand(ctx context.Context) *cli.Command {
return nil
},
},
{
Name: "list-services",
Usage: "lists Kubernetes services in all namespaces",
Flags: []cli.Flag{
&cli.StringFlag{Name: "kubeconfig", Usage: "absolute path to the kubeconfig file", Required: false},
},
Action: func(c *cli.Context) error {
kubeconfigPath := c.String("kubeconfig")
clientset, err := k8s.BuildKubeClient(kubeconfigPath)
if err != nil {
return err
}
return k8s.ListServices(ctx, clientset)
},
},
},
}
return cmd
Expand Down
43 changes: 40 additions & 3 deletions pkg/cmd/config/command_test.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,66 @@
package config_test

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"

"github.com/cloudzero/cloudzero-agent-validator/pkg/cmd/config"
"github.com/cloudzero/cloudzero-agent-validator/pkg/k8s"
)

func TestGenerate(t *testing.T) {
// Create a fake clientset with some services
clientset := fake.NewSimpleClientset(
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "kube-state-metrics",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{Port: 8080},
},
},
},
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "node-exporter",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{Port: 9100},
},
},
},
)

ctx, _ := context.WithCancel(context.Background())

// Fetch service URLs
kubeStateMetricsURL, nodeExporterURL, err := k8s.GetServiceURLs(ctx, clientset)
assert.NoError(t, err)

values := map[string]interface{}{
"ChartVerson": "1.0.0",
"AgentVersion": "1.0.0",
"AccountID": "123456789",
"ClusterName": "test-cluster",
"Region": "us-west-2",
"CloudzeroHost": "https://cloudzero.com",
"KubeStateMetricsURL": "http://kube-state-metrics.your-namespace.svc.cluster.local:8080",
"PromNodeExporterURL": "http://node-exporter.your-namespace.svc.cluster.local:9100",
"KubeStateMetricsURL": kubeStateMetricsURL,
"PromNodeExporterURL": nodeExporterURL,
}

outputFile := "test_output.yml"

err := config.Generate(values, outputFile)
err = config.Generate(values, outputFile)
assert.NoError(t, err)

// Verify that the output file exists
Expand Down
32 changes: 25 additions & 7 deletions pkg/k8s/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package k8s
import (
"context"
"fmt"
"strings"

"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -25,19 +26,36 @@ func BuildKubeClient(kubeconfigPath string) (kubernetes.Interface, error) {
return clientset, nil
}

// ListServices lists all Kubernetes services in all namespaces
func ListServices(ctx context.Context, clientset kubernetes.Interface) error {
// GetServiceURLs retrieves the URLs for services containing 'kube-state-metrics' and 'node-exporter' substrings
func GetServiceURLs(ctx context.Context, clientset kubernetes.Interface) (string, string, error) {
// List all services in all namespaces
services, err := clientset.CoreV1().Services("").List(ctx, metav1.ListOptions{})
if err != nil {
return errors.Wrap(err, "listing services")
return "", "", errors.Wrap(err, "listing services")
}

// Print the names and namespaces of the services
fmt.Println("Services in all namespaces:")
var kubeStateMetricsURL, nodeExporterURL string

// Filter services for substrings 'kube-state-metrics' and 'node-exporter' and generate URLs
for _, service := range services.Items {
fmt.Printf(" - %s (Namespace: %s)\n", service.Name, service.Namespace)
if strings.Contains(service.Name, "kube-state-metrics") {
if len(service.Spec.Ports) > 0 {
kubeStateMetricsURL = fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", service.Name, service.Namespace, service.Spec.Ports[0].Port)
}
} else if strings.Contains(service.Name, "node-exporter") {
if len(service.Spec.Ports) > 0 {
nodeExporterURL = fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", service.Name, service.Namespace, service.Spec.Ports[0].Port)
}
}
}

if kubeStateMetricsURL == "" {
return "", "", fmt.Errorf("kube-state-metrics service not found. Please install kube-state-metrics")
}

if nodeExporterURL == "" {
return "", "", fmt.Errorf("node-exporter service not found. Please install node-exporter")
}

return nil
return kubeStateMetricsURL, nodeExporterURL, nil
}
108 changes: 90 additions & 18 deletions pkg/k8s/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,107 @@ import (
"context"
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"

"github.com/cloudzero/cloudzero-agent-validator/pkg/k8s"
)

// TestListServices tests the ListServices function
func TestListServices(t *testing.T) {
// Create a fake clientset with some services
clientset := fake.NewSimpleClientset(
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "service1",
Namespace: "default",
// TestGetServiceURLs tests the GetServiceURLs function
func TestGetServiceURLs(t *testing.T) {
tests := []struct {
name string
services []corev1.Service
expectedKubeStateURL string
expectedNodeExporterURL string
expectError bool
}{
{
name: "Both services found",
services: []corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "kube-state-metrics",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{Port: 8080},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "node-exporter",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{Port: 9100},
},
},
},
},
expectedKubeStateURL: "http://kube-state-metrics.default.svc.cluster.local:8080",
expectedNodeExporterURL: "http://node-exporter.default.svc.cluster.local:9100",
expectError: false,
},
&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "service2",
Namespace: "kube-system",
{
name: "Kube-state-metrics service not found",
services: []corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "node-exporter",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{Port: 9100},
},
},
},
},
expectedKubeStateURL: "",
expectedNodeExporterURL: "",
expectError: true,
},
)
{
name: "Node-exporter service not found",
services: []corev1.Service{
{
ObjectMeta: metav1.ObjectMeta{
Name: "kube-state-metrics",
Namespace: "default",
},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{Port: 8080},
},
},
},
},
expectedKubeStateURL: "",
expectedNodeExporterURL: "",
expectError: true,
},
}

ctx, _ := context.WithCancel(context.Background())
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clientset := fake.NewSimpleClientset(&corev1.ServiceList{Items: tt.services})

// Test listing services
err := k8s.ListServices(ctx, clientset)
assert.NoError(t, err)
kubeStateMetricsURL, nodeExporterURL, err := k8s.GetServiceURLs(context.Background(), clientset)
if (err != nil) != tt.expectError {
t.Errorf("GetServiceURLs() error = %v, expectError %v", err, tt.expectError)
return
}
if kubeStateMetricsURL != tt.expectedKubeStateURL {
t.Errorf("GetServiceURLs() kubeStateMetricsURL = %v, expected %v", kubeStateMetricsURL, tt.expectedKubeStateURL)
}
if nodeExporterURL != tt.expectedNodeExporterURL {
t.Errorf("GetServiceURLs() nodeExporterURL = %v, expected %v", nodeExporterURL, tt.expectedNodeExporterURL)
}
})
}
}

0 comments on commit b2304d9

Please sign in to comment.