Skip to content

Commit 27c165c

Browse files
committed
Add kiali tools
Signed-off-by: Alberto Gutierrez <aljesusg@gmail.com>
1 parent 73b7a32 commit 27c165c

25 files changed

+2202
-19
lines changed

pkg/kiali/endpoints.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ package kiali
33
// Kiali API endpoint paths shared across this package.
44
const (
55
// MeshGraph is the Kiali API path that returns the mesh graph/status.
6-
MeshGraph = "/api/mesh/graph"
7-
AuthInfo = "/api/auth/info"
6+
AuthInfoEndpoint = "/api/auth/info"
7+
MeshGraphEndpoint = "/api/mesh/graph"
8+
GraphEndpoint = "/api/namespaces/graph"
9+
HealthEndpoint = "/api/clusters/health"
10+
IstioConfigEndpoint = "/api/istio/config"
11+
IstioObjectEndpoint = "/api/namespaces/%s/istio/%s/%s/%s/%s"
12+
IstioObjectCreateEndpoint = "/api/namespaces/%s/istio/%s/%s/%s"
13+
NamespacesEndpoint = "/api/namespaces"
14+
PodDetailsEndpoint = "/api/namespaces/%s/pods/%s"
15+
PodsLogsEndpoint = "/api/namespaces/%s/pods/%s/logs"
16+
ServicesEndpoint = "/api/clusters/services"
17+
ServiceDetailsEndpoint = "/api/namespaces/%s/services/%s"
18+
ServiceMetricsEndpoint = "/api/namespaces/%s/services/%s/metrics"
19+
AppTracesEndpoint = "/api/namespaces/%s/apps/%s/traces"
20+
ServiceTracesEndpoint = "/api/namespaces/%s/services/%s/traces"
21+
WorkloadTracesEndpoint = "/api/namespaces/%s/workloads/%s/traces"
22+
WorkloadsEndpoint = "/api/clusters/workloads"
23+
WorkloadDetailsEndpoint = "/api/namespaces/%s/workloads/%s"
24+
WorkloadMetricsEndpoint = "/api/namespaces/%s/workloads/%s/metrics"
25+
ValidationsEndpoint = "/api/istio/validations"
26+
ValidationsListEndpoint = "/api/istio/validations"
827
)

pkg/kiali/graph.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package kiali
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"strings"
8+
)
9+
10+
// Graph calls the Kiali graph API using the provided Authorization header value.
11+
// `namespaces` may contain zero, one or many namespaces. If empty, the API may return an empty graph
12+
// or the server default, depending on Kiali configuration.
13+
func (k *Kiali) Graph(ctx context.Context, namespaces []string) (string, error) {
14+
u, err := url.Parse(GraphEndpoint)
15+
if err != nil {
16+
return "", err
17+
}
18+
q := u.Query()
19+
// Static graph parameters per requirements
20+
q.Set("duration", "60s")
21+
q.Set("graphType", "versionedApp")
22+
q.Set("includeIdleEdges", "false")
23+
q.Set("injectServiceNodes", "true")
24+
q.Set("boxBy", "cluster,namespace,app")
25+
q.Set("ambientTraffic", "none")
26+
q.Set("appenders", "deadNode,istio,serviceEntry,meshCheck,workloadEntry,health")
27+
q.Set("rateGrpc", "requests")
28+
q.Set("rateHttp", "requests")
29+
q.Set("rateTcp", "sent")
30+
// Optional namespaces param
31+
cleaned := make([]string, 0, len(namespaces))
32+
for _, ns := range namespaces {
33+
ns = strings.TrimSpace(ns)
34+
if ns != "" {
35+
cleaned = append(cleaned, ns)
36+
}
37+
}
38+
if len(cleaned) > 0 {
39+
q.Set("namespaces", strings.Join(cleaned, ","))
40+
}
41+
u.RawQuery = q.Encode()
42+
endpoint := u.String()
43+
44+
return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil)
45+
}

pkg/kiali/health.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package kiali
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
)
8+
9+
// Health returns health status for apps, workloads, and services across namespaces.
10+
// Parameters:
11+
// - namespaces: comma-separated list of namespaces (optional, if empty returns health for all accessible namespaces)
12+
// - queryParams: optional query parameters map for filtering health data (e.g., "type", "rateInterval", "queryTime")
13+
// - type: health type - "app", "service", or "workload" (default: "app")
14+
// - rateInterval: rate interval for fetching error rate (default: "10m")
15+
// - queryTime: Unix timestamp for the prometheus query (optional)
16+
func (k *Kiali) Health(ctx context.Context, namespaces string, queryParams map[string]string) (string, error) {
17+
// Build query parameters
18+
u, err := url.Parse(HealthEndpoint)
19+
if err != nil {
20+
return "", err
21+
}
22+
q := u.Query()
23+
24+
// Add namespaces if provided
25+
if namespaces != "" {
26+
q.Set("namespaces", namespaces)
27+
}
28+
29+
// Add optional query parameters
30+
if len(queryParams) > 0 {
31+
for key, value := range queryParams {
32+
q.Set(key, value)
33+
}
34+
}
35+
36+
u.RawQuery = q.Encode()
37+
endpoint := u.String()
38+
39+
return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil)
40+
}

pkg/kiali/istio.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package kiali
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"strings"
9+
)
10+
11+
// IstioConfig calls the Kiali Istio config API to get all Istio objects in the mesh.
12+
// Returns the full YAML resources and additional details about each object.
13+
func (k *Kiali) IstioConfig(ctx context.Context) (string, error) {
14+
endpoint := IstioConfigEndpoint + "?validate=true"
15+
16+
return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil)
17+
}
18+
19+
// IstioObjectDetails returns detailed information about a specific Istio object.
20+
// Parameters:
21+
// - namespace: the namespace containing the Istio object
22+
// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io")
23+
// - version: the API version (e.g., "v1", "v1beta1")
24+
// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute")
25+
// - name: the name of the resource
26+
func (k *Kiali) IstioObjectDetails(ctx context.Context, namespace, group, version, kind, name string) (string, error) {
27+
if namespace == "" {
28+
return "", fmt.Errorf("namespace is required")
29+
}
30+
if group == "" {
31+
return "", fmt.Errorf("group is required")
32+
}
33+
if version == "" {
34+
return "", fmt.Errorf("version is required")
35+
}
36+
if kind == "" {
37+
return "", fmt.Errorf("kind is required")
38+
}
39+
if name == "" {
40+
return "", fmt.Errorf("name is required")
41+
}
42+
endpoint := fmt.Sprintf(IstioObjectEndpoint+"?validate=true&help=true",
43+
url.PathEscape(namespace),
44+
url.PathEscape(group),
45+
url.PathEscape(version),
46+
url.PathEscape(kind),
47+
url.PathEscape(name))
48+
49+
return k.executeRequest(ctx, http.MethodGet, endpoint, "", nil)
50+
}
51+
52+
// IstioObjectPatch patches an existing Istio object using PATCH method.
53+
// Parameters:
54+
// - namespace: the namespace containing the Istio object
55+
// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io")
56+
// - version: the API version (e.g., "v1", "v1beta1")
57+
// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute")
58+
// - name: the name of the resource
59+
// - jsonPatch: the JSON patch data to apply
60+
func (k *Kiali) IstioObjectPatch(ctx context.Context, namespace, group, version, kind, name, jsonPatch string) (string, error) {
61+
if namespace == "" {
62+
return "", fmt.Errorf("namespace is required")
63+
}
64+
if group == "" {
65+
return "", fmt.Errorf("group is required")
66+
}
67+
if version == "" {
68+
return "", fmt.Errorf("version is required")
69+
}
70+
if kind == "" {
71+
return "", fmt.Errorf("kind is required")
72+
}
73+
if name == "" {
74+
return "", fmt.Errorf("name is required")
75+
}
76+
if jsonPatch == "" {
77+
return "", fmt.Errorf("json patch data is required")
78+
}
79+
endpoint := fmt.Sprintf(IstioObjectEndpoint,
80+
url.PathEscape(namespace),
81+
url.PathEscape(group),
82+
url.PathEscape(version),
83+
url.PathEscape(kind),
84+
url.PathEscape(name))
85+
86+
return k.executeRequest(ctx, http.MethodPatch, endpoint, "application/json", strings.NewReader(jsonPatch))
87+
}
88+
89+
// IstioObjectCreate creates a new Istio object using POST method.
90+
// Parameters:
91+
// - namespace: the namespace where the Istio object will be created
92+
// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io")
93+
// - version: the API version (e.g., "v1", "v1beta1")
94+
// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute")
95+
// - jsonData: the JSON data for the new object
96+
func (k *Kiali) IstioObjectCreate(ctx context.Context, namespace, group, version, kind, jsonData string) (string, error) {
97+
if namespace == "" {
98+
return "", fmt.Errorf("namespace is required")
99+
}
100+
if group == "" {
101+
return "", fmt.Errorf("group is required")
102+
}
103+
if version == "" {
104+
return "", fmt.Errorf("version is required")
105+
}
106+
if kind == "" {
107+
return "", fmt.Errorf("kind is required")
108+
}
109+
if jsonData == "" {
110+
return "", fmt.Errorf("json data is required")
111+
}
112+
endpoint := fmt.Sprintf(IstioObjectCreateEndpoint,
113+
url.PathEscape(namespace),
114+
url.PathEscape(group),
115+
url.PathEscape(version),
116+
url.PathEscape(kind))
117+
118+
return k.executeRequest(ctx, http.MethodPost, endpoint, "application/json", strings.NewReader(jsonData))
119+
}
120+
121+
// IstioObjectDelete deletes an existing Istio object using DELETE method.
122+
// Parameters:
123+
// - namespace: the namespace containing the Istio object
124+
// - group: the API group (e.g., "networking.istio.io", "gateway.networking.k8s.io")
125+
// - version: the API version (e.g., "v1", "v1beta1")
126+
// - kind: the resource kind (e.g., "DestinationRule", "VirtualService", "HTTPRoute", "Gateway")
127+
// - name: the name of the resource
128+
func (k *Kiali) IstioObjectDelete(ctx context.Context, namespace, group, version, kind, name string) (string, error) {
129+
if namespace == "" {
130+
return "", fmt.Errorf("namespace is required")
131+
}
132+
if group == "" {
133+
return "", fmt.Errorf("group is required")
134+
}
135+
if version == "" {
136+
return "", fmt.Errorf("version is required")
137+
}
138+
if kind == "" {
139+
return "", fmt.Errorf("kind is required")
140+
}
141+
if name == "" {
142+
return "", fmt.Errorf("name is required")
143+
}
144+
endpoint := fmt.Sprintf(IstioObjectEndpoint,
145+
url.PathEscape(namespace),
146+
url.PathEscape(group),
147+
url.PathEscape(version),
148+
url.PathEscape(kind),
149+
url.PathEscape(name))
150+
151+
return k.executeRequest(ctx, http.MethodDelete, endpoint, "", nil)
152+
}

pkg/kiali/kiali.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,39 @@ func (k *Kiali) authorizationHeader() string {
8484
return "Bearer " + token
8585
}
8686

87-
// executeRequest executes an HTTP request and handles common error scenarios.
88-
func (k *Kiali) executeRequest(ctx context.Context, endpoint string) (string, error) {
87+
// executeRequest executes an HTTP request (optionally with a body) and handles common error scenarios.
88+
func (k *Kiali) executeRequest(ctx context.Context, method, endpoint, contentType string, body io.Reader) (string, error) {
89+
if method == "" {
90+
method = http.MethodGet
91+
}
8992
ApiCallURL, err := k.validateAndGetURL(endpoint)
9093
if err != nil {
9194
return "", err
9295
}
93-
94-
klog.V(0).Infof("Kiali Call URL: %s", ApiCallURL)
95-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ApiCallURL, nil)
96+
klog.V(0).Infof("kiali API call: %s %s", method, ApiCallURL)
97+
req, err := http.NewRequestWithContext(ctx, method, ApiCallURL, body)
9698
if err != nil {
9799
return "", err
98100
}
99101
authHeader := k.authorizationHeader()
100102
if authHeader != "" {
101103
req.Header.Set("Authorization", authHeader)
102104
}
105+
if contentType != "" {
106+
req.Header.Set("Content-Type", contentType)
107+
}
103108
client := k.createHTTPClient()
104109
resp, err := client.Do(req)
105110
if err != nil {
106111
return "", err
107112
}
108113
defer func() { _ = resp.Body.Close() }()
109-
body, _ := io.ReadAll(resp.Body)
114+
respBody, _ := io.ReadAll(resp.Body)
110115
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
111-
if len(body) > 0 {
112-
return "", fmt.Errorf("kiali API error: %s", strings.TrimSpace(string(body)))
116+
if len(respBody) > 0 {
117+
return "", fmt.Errorf("kiali API error: %s", strings.TrimSpace(string(respBody)))
113118
}
114119
return "", fmt.Errorf("kiali API error: status %d", resp.StatusCode)
115120
}
116-
return string(body), nil
121+
return string(respBody), nil
117122
}

pkg/kiali/kiali_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (s *KialiSuite) TestNewKiali_InvalidConfig() {
5353
url = "://invalid-url"
5454
`))
5555
s.Error(err, "Expected error reading invalid config")
56-
s.ErrorContains(err, "kiali-url must be a valid URL", "Unexpected error message")
56+
s.ErrorContains(err, "url must be a valid URL", "Unexpected error message")
5757
s.Nil(cfg, "Unexpected Kiali config")
5858
}
5959

@@ -108,7 +108,7 @@ func (s *KialiSuite) TestExecuteRequest() {
108108
`, s.MockServer.Config().Host))))
109109
k := NewKiali(s.Config, s.MockServer.Config())
110110

111-
out, err := k.executeRequest(s.T().Context(), "/api/ping?q=1")
111+
out, err := k.executeRequest(s.T().Context(), http.MethodGet, "/api/ping?q=1", "", nil)
112112
s.Require().NoError(err, "Expected no error executing request")
113113
s.Run("auth header set", func() {
114114
s.Equal("Bearer token-xyz", seenAuth, "Unexpected Authorization header")
@@ -123,4 +123,4 @@ func (s *KialiSuite) TestExecuteRequest() {
123123

124124
func TestKiali(t *testing.T) {
125125
suite.Run(t, new(KialiSuite))
126-
}
126+
}

0 commit comments

Comments
 (0)