volumes.go 7.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// Package k8s provides Kubernetes-specific functionality for checkpoint operations.
// This includes volume type discovery via K8s API and containerd container discovery.
package k8s

import (
	"context"
	"fmt"
	"strings"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

// VolumeInfo contains Kubernetes volume information for a mount.
type VolumeInfo struct {
	VolumeName string // Name from pod.spec.volumes[].name
	VolumeType string // Type: emptyDir, configMap, secret, persistentVolumeClaim, etc.
	MountPath  string // Container path from volumeMounts[].mountPath
	SubPath    string // SubPath if specified
	ReadOnly   bool   // Whether mount is read-only

	// Type-specific details
	ConfigMapName string // For configMap volumes
	SecretName    string // For secret volumes
	PVCName       string // For persistentVolumeClaim volumes
}

// K8sClient wraps the Kubernetes clientset for volume discovery.
type K8sClient struct {
	clientset *kubernetes.Clientset
}

// NewK8sClient creates a new Kubernetes client.
// It attempts in-cluster config first, then falls back to kubeconfig.
func NewK8sClient() (*K8sClient, error) {
	config, err := rest.InClusterConfig()
	if err != nil {
		// Fall back to kubeconfig for local development
		config, err = clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
		if err != nil {
			return nil, fmt.Errorf("failed to create k8s config: %w", err)
		}
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, fmt.Errorf("failed to create k8s clientset: %w", err)
	}

	return &K8sClient{clientset: clientset}, nil
}

// NewK8sClientWithConfig creates a client with explicit config.
func NewK8sClientWithConfig(config *rest.Config) (*K8sClient, error) {
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, fmt.Errorf("failed to create k8s clientset: %w", err)
	}
	return &K8sClient{clientset: clientset}, nil
}

// GetPodVolumes returns volume information for all mounts in a container.
// Returns a map from mount path to VolumeInfo.
func (c *K8sClient) GetPodVolumes(ctx context.Context, namespace, podName, containerName string) (map[string]*VolumeInfo, error) {
	pod, err := c.clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
	if err != nil {
		return nil, fmt.Errorf("failed to get pod %s/%s: %w", namespace, podName, err)
	}

	return ExtractVolumeInfo(pod, containerName)
}

// ExtractVolumeInfo extracts volume information from a Pod spec.
// This is the core logic that maps volumeMounts to volumes and determines types.
func ExtractVolumeInfo(pod *corev1.Pod, containerName string) (map[string]*VolumeInfo, error) {
	// Build volume name -> type mapping from pod.spec.volumes
	volumeTypes := make(map[string]*volumeDetails)
	for _, vol := range pod.Spec.Volumes {
		volumeTypes[vol.Name] = getVolumeDetails(&vol)
	}

	// Find the target container
	var container *corev1.Container
	for i := range pod.Spec.Containers {
		if pod.Spec.Containers[i].Name == containerName {
			container = &pod.Spec.Containers[i]
			break
		}
	}
	if container == nil {
		// Try init containers
		for i := range pod.Spec.InitContainers {
			if pod.Spec.InitContainers[i].Name == containerName {
				container = &pod.Spec.InitContainers[i]
				break
			}
		}
	}
	if container == nil {
		return nil, fmt.Errorf("container %s not found in pod", containerName)
	}

	// Build mount path -> volume info mapping
	result := make(map[string]*VolumeInfo)
	for _, mount := range container.VolumeMounts {
		details, ok := volumeTypes[mount.Name]
		if !ok {
			continue // Mount references unknown volume
		}

		result[mount.MountPath] = &VolumeInfo{
			VolumeName:    mount.Name,
			VolumeType:    details.volumeType,
			MountPath:     mount.MountPath,
			SubPath:       mount.SubPath,
			ReadOnly:      mount.ReadOnly,
			ConfigMapName: details.configMapName,
			SecretName:    details.secretName,
			PVCName:       details.pvcName,
		}
	}

	return result, nil
}

// volumeDetails holds extracted volume type information.
type volumeDetails struct {
	volumeType    string
	configMapName string
	secretName    string
	pvcName       string
}

// getVolumeDetails extracts type and details from a Volume spec.
func getVolumeDetails(vol *corev1.Volume) *volumeDetails {
	d := &volumeDetails{volumeType: "unknown"}

	switch {
	case vol.EmptyDir != nil:
		d.volumeType = "emptyDir"
	case vol.ConfigMap != nil:
		d.volumeType = "configMap"
		d.configMapName = vol.ConfigMap.Name
	case vol.Secret != nil:
		d.volumeType = "secret"
		d.secretName = vol.Secret.SecretName
	case vol.PersistentVolumeClaim != nil:
		d.volumeType = "persistentVolumeClaim"
		d.pvcName = vol.PersistentVolumeClaim.ClaimName
	case vol.HostPath != nil:
		d.volumeType = "hostPath"
	case vol.Projected != nil:
		d.volumeType = "projected"
	case vol.DownwardAPI != nil:
		d.volumeType = "downwardAPI"
	case vol.CSI != nil:
		d.volumeType = "csi"
	case vol.NFS != nil:
		d.volumeType = "nfs"
	case vol.ISCSI != nil:
		d.volumeType = "iscsi"
	case vol.GCEPersistentDisk != nil:
		d.volumeType = "gcePersistentDisk"
	case vol.AWSElasticBlockStore != nil:
		d.volumeType = "awsElasticBlockStore"
	case vol.AzureDisk != nil:
		d.volumeType = "azureDisk"
	case vol.AzureFile != nil:
		d.volumeType = "azureFile"
	case vol.CephFS != nil:
		d.volumeType = "cephfs"
	case vol.Cinder != nil:
		d.volumeType = "cinder"
	case vol.FC != nil:
		d.volumeType = "fc"
	case vol.FlexVolume != nil:
		d.volumeType = "flexVolume"
	case vol.Flocker != nil:
		d.volumeType = "flocker"
	case vol.GitRepo != nil:
		d.volumeType = "gitRepo"
	case vol.Glusterfs != nil:
		d.volumeType = "glusterfs"
	case vol.PhotonPersistentDisk != nil:
		d.volumeType = "photonPersistentDisk"
	case vol.PortworxVolume != nil:
		d.volumeType = "portworxVolume"
	case vol.Quobyte != nil:
		d.volumeType = "quobyte"
	case vol.RBD != nil:
		d.volumeType = "rbd"
	case vol.ScaleIO != nil:
		d.volumeType = "scaleIO"
	case vol.StorageOS != nil:
		d.volumeType = "storageos"
	case vol.VsphereVolume != nil:
		d.volumeType = "vsphereVolume"
	case vol.Ephemeral != nil:
		d.volumeType = "ephemeral"
	}

	return d
}

// DetectVolumeTypeFromPath attempts to identify volume type from kubelet path patterns.
// This is a best-effort fallback; accurate volume types require K8s API access via GetPodVolumes.
func DetectVolumeTypeFromPath(hostPath string) (volumeType, volumeName string) {
	volumeType = "unknown"
	volumeName = ""

	// Map of path patterns to volume types
	patterns := map[string]string{
		"/kubernetes.io~empty-dir/":             "emptyDir",
		"/kubernetes.io~configmap/":             "configMap",
		"/kubernetes.io~secret/":                "secret",
		"/kubernetes.io~projected/":             "projected",
		"/kubernetes.io~downward-api/":          "downwardAPI",
		"/kubernetes.io~persistentvolumeclaim/": "persistentVolumeClaim",
		"/kubernetes.io~hostpath/":              "hostPath",
	}

	for pattern, vType := range patterns {
		if strings.Contains(hostPath, pattern) {
			volumeType = vType
			// Extract volume name from path
			parts := strings.Split(hostPath, pattern)
			if len(parts) > 1 {
				volumeName = strings.Split(parts[1], "/")[0]
			}
			break
		}
	}

	return volumeType, volumeName
}