-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
expose.go
350 lines (310 loc) · 12 KB
/
expose.go
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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
package expose
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/spf13/cobra"
v1 "k8s.io/api/core/v1"
k8smetav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/tools/clientcmd"
v12 "kubevirt.io/client-go/api/v1"
"kubevirt.io/client-go/kubecli"
"kubevirt.io/kubevirt/pkg/virtctl/templates"
)
const (
COMMAND_EXPOSE = "expose"
)
type Command struct {
clientConfig clientcmd.ClientConfig
command string
}
// holding flag information
var serviceName string
var clusterIP string
var externalIP string
var loadBalancerIP string
var port int32
var strProtocol string
var strTargetPort string
var strServiceType string
var portName string
var strIPFamily string
var strIPFamilyPolicy string
// NewExposeCommand generates a new "expose" command
func NewExposeCommand(clientConfig clientcmd.ClientConfig) *cobra.Command {
cmd := &cobra.Command{
Use: "expose (TYPE NAME)",
Short: "Expose a virtual machine instance, virtual machine, or virtual machine instance replica set as a new service.",
Long: `Looks up a virtual machine instance, virtual machine or virtual machine instance replica set by name and use its selector as the selector for a new service on the specified port.
A virtual machine instance replica set will be exposed as a service only if its selector is convertible to a selector that service supports, i.e. when the selector contains only the matchLabels component.
Note that if no port is specified via --port and the exposed resource has multiple ports, all will be re-used by the new service.
Also if no labels are specified, the new service will re-use the labels from the resource it exposes.
Possible types are (case insensitive, both single and plurant forms):
virtualmachineinstance (vmi), virtualmachine (vm), virtualmachineinstancereplicaset (vmirs)`,
Example: usage(),
Args: templates.ExactArgs("expose", 2),
RunE: func(cmd *cobra.Command, args []string) error {
c := Command{command: COMMAND_EXPOSE, clientConfig: clientConfig}
return c.RunE(args)
},
}
// flags for the "expose" command
cmd.Flags().StringVar(&serviceName, "name", "", "Name of the service created for the exposure of the VM.")
cmd.MarkFlagRequired("name")
cmd.Flags().StringVar(&clusterIP, "cluster-ip", "", "ClusterIP to be assigned to the service. Leave empty to auto-allocate, or set to 'None' to create a headless service.")
cmd.Flags().StringVar(&externalIP, "external-ip", "", "Additional external IP address (not managed by the cluster) to accept for the service. If this IP is routed to a node, the service can be accessed by this IP in addition to its generated service IP. Optional.")
cmd.Flags().StringVar(&loadBalancerIP, "load-balancer-ip", "", "IP to assign to the Load Balancer. If empty, an ephemeral IP will be created and used.")
cmd.Flags().Int32Var(&port, "port", 0, "The port that the service should serve on.")
cmd.Flags().StringVar(&strProtocol, "protocol", "TCP", "The network protocol for the service to be created.")
cmd.Flags().StringVar(&strTargetPort, "target-port", "", "Name or number for the port on the VM that the service should direct traffic to. Optional.")
cmd.Flags().StringVar(&strServiceType, "type", "ClusterIP", "Type for this service: ClusterIP, NodePort, or LoadBalancer.")
cmd.Flags().StringVar(&portName, "port-name", "", "Name of the port. Optional.")
cmd.Flags().StringVar(&strIPFamily, "ip-family", "IPv4", "IP family over which the service will be exposed. Valid values are 'IPv4', 'IPv6', 'IPv4,IPv6' or 'IPv6,IPv4'")
cmd.Flags().StringVar(&strIPFamilyPolicy, "ip-family-policy", "", "IP family policy defines whether the service can use IPv4, IPv6, or both. Valid values are 'SingleStack', 'PreferDualStack' or 'RequireDualStack'")
cmd.SetUsageTemplate(templates.UsageTemplate())
return cmd
}
func usage() string {
usage := ` # Expose SSH to a virtual machine instance called 'myvm' on each node via a NodePort service:
{{ProgramName}} expose vmi myvm --port=22 --name=myvm-ssh --type=NodePort
# Expose all defined pod-network ports of a virtual machine instance replicaset on a service:
{{ProgramName}} expose vmirs myvmirs --name=vmirs-service
# Expose port 8080 as port 80 from a virtual machine instance replicaset on a service:
{{ProgramName}} expose vmirs myvmirs --port=80 --target-port=8080 --name=vmirs-service`
return usage
}
// executing the "expose" command
func (o *Command) RunE(args []string) error {
// first argument is type of VM: VMI, VM or VMIRS
vmType := strings.ToLower(args[0])
// second argument must be name of the VM
vmName := args[1]
// these are used to convert the flag values into service spec values
var protocol v1.Protocol
var targetPort intstr.IntOrString
var serviceType v1.ServiceType
// convert from integer to the IntOrString type
targetPort = intstr.Parse(strTargetPort)
// convert from string to the protocol enum
switch strProtocol {
case "TCP":
protocol = v1.ProtocolTCP
case "UDP":
protocol = v1.ProtocolUDP
default:
return fmt.Errorf("unknown protocol: %s", strProtocol)
}
// convert from string to the service type enum
switch strServiceType {
case "ClusterIP":
serviceType = v1.ServiceTypeClusterIP
case "NodePort":
serviceType = v1.ServiceTypeNodePort
case "LoadBalancer":
serviceType = v1.ServiceTypeLoadBalancer
case "ExternalName":
return fmt.Errorf("type: %s not supported", strServiceType)
default:
return fmt.Errorf("unknown service type: %s", strServiceType)
}
ipFamilies, err := convertIPFamily(strIPFamily)
if err != nil {
return err
}
ipFamilyPolicy, err := convertIPFamilyPolicy(strIPFamilyPolicy)
if err != nil {
return err
}
// get the namespace
namespace, _, err := o.clientConfig.Namespace()
if err != nil {
return err
}
// get the client
virtClient, err := kubecli.GetKubevirtClientFromClientConfig(o.clientConfig)
if err != nil {
return fmt.Errorf("cannot obtain KubeVirt client: %v", err)
}
// does a plain quorum read from the apiserver
options := k8smetav1.GetOptions{}
var serviceSelector map[string]string
ports := []v1.ServicePort{}
switch vmType {
case "vmi", "vmis", "virtualmachineinstance", "virtualmachineinstances":
// get the VM
vmi, err := virtClient.VirtualMachineInstance(namespace).Get(vmName, &options)
if err != nil {
return fmt.Errorf("error fetching VirtualMachineInstance: %v", err)
}
serviceSelector = vmi.ObjectMeta.Labels
ports = podNetworkPorts(&vmi.Spec)
// remove unwanted labels
delete(serviceSelector, "kubevirt.io/nodeName")
case "vm", "vms", "virtualmachine", "virtualmachines":
// get the VM
vm, err := virtClient.VirtualMachine(namespace).Get(vmName, &options)
if err != nil {
return fmt.Errorf("error fetching Virtual Machine: %v", err)
}
if vm.Spec.Template != nil {
ports = podNetworkPorts(&vm.Spec.Template.Spec)
}
serviceSelector = vm.Spec.Template.ObjectMeta.Labels
case "vmirs", "vmirss", "virtualmachineinstancereplicaset", "virtualmachineinstancereplicasets":
// get the VM replica set
vmirs, err := virtClient.ReplicaSet(namespace).Get(vmName, options)
if err != nil {
return fmt.Errorf("error fetching VirtualMachineInstance ReplicaSet: %v", err)
}
if len(vmirs.Spec.Selector.MatchExpressions) > 0 {
return fmt.Errorf("cannot expose VirtualMachineInstance ReplicaSet with match expressions")
}
if vmirs.Spec.Template != nil {
ports = podNetworkPorts(&vmirs.Spec.Template.Spec)
}
serviceSelector = vmirs.Spec.Selector.MatchLabels
default:
return fmt.Errorf("unsupported resource type: %s", vmType)
}
if len(serviceSelector) == 0 {
return fmt.Errorf("missing label information for %s: %s", vmType, vmName)
}
if port == 0 && len(ports) == 0 {
return fmt.Errorf("couldn't find port via --port flag or introspection")
} else if port != 0 {
ports = []v1.ServicePort{{Name: portName, Protocol: protocol, Port: port, TargetPort: targetPort}}
}
// actually create the service
service := &v1.Service{
ObjectMeta: k8smetav1.ObjectMeta{
Name: serviceName,
Namespace: namespace,
},
Spec: v1.ServiceSpec{
Ports: ports,
Selector: serviceSelector,
ClusterIP: clusterIP,
Type: serviceType,
LoadBalancerIP: loadBalancerIP,
IPFamilies: ipFamilies,
},
}
// set external IP if provided
if len(externalIP) > 0 {
service.Spec.ExternalIPs = []string{externalIP}
}
if ipFamilyPolicy != "" {
service.Spec.IPFamilyPolicy = &ipFamilyPolicy
}
major, minor, err := serverVersion(virtClient)
if err != nil {
return err
}
if major > 1 || (major == 1 && minor >= 20) {
_, err = virtClient.CoreV1().Services(namespace).Create(context.Background(), service, k8smetav1.CreateOptions{})
if err != nil {
return fmt.Errorf("service creation failed for k8s >= 1.20: %v", err)
}
// For k8s < 1.20 we have to "migrate" the "ipFamilies" field to
// "ipFamily" we do this using an unstructured approach
} else {
if len(ipFamilies) > 1 {
return fmt.Errorf("k8s < 1.20 doesn't support multiple ip families")
}
if ipFamilyPolicy != "" {
return fmt.Errorf("k8s < 1.20 doesn't support 'ipFamilyPolicy'")
}
// convert the Service to unstructured.Unstructured
unstructuredService, err := runtime.DefaultUnstructuredConverter.ToUnstructured(service)
if err != nil {
return err
}
// Add ipFamily field with proper content
err = unstructured.SetNestedField(unstructuredService, string(ipFamilies[0]), "spec", "ipFamily")
if err != nil {
return err
}
// try to create the service on the cluster
_, err = virtClient.DynamicClient().Resource(schema.GroupVersionResource{Version: "v1", Resource: "services"}).Namespace(namespace).Create(context.Background(), &unstructured.Unstructured{Object: unstructuredService}, k8smetav1.CreateOptions{})
if err != nil {
return fmt.Errorf("service creation failed for k8s < 1.20: %v", err)
}
}
fmt.Printf("Service %s successfully exposed for %s %s\n", serviceName, vmType, vmName)
return nil
}
func convertIPFamily(strIPFamily string) ([]v1.IPFamily, error) {
switch strings.ToLower(strIPFamily) {
case "ipv4":
return []v1.IPFamily{v1.IPv4Protocol}, nil
case "ipv6":
return []v1.IPFamily{v1.IPv6Protocol}, nil
case "ipv4,ipv6":
return []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol}, nil
case "ipv6,ipv4":
return []v1.IPFamily{v1.IPv6Protocol, v1.IPv4Protocol}, nil
default:
return nil, fmt.Errorf("unknown IPFamily/s: %s", strIPFamily)
}
}
func convertIPFamilyPolicy(strIPFamilyPolicy string) (v1.IPFamilyPolicyType, error) {
switch strings.ToLower(strIPFamilyPolicy) {
case "":
return "", nil
case "singlestack":
return v1.IPFamilyPolicySingleStack, nil
case "preferdualstack":
return v1.IPFamilyPolicyPreferDualStack, nil
case "requiredualstack":
return v1.IPFamilyPolicyRequireDualStack, nil
default:
return "", fmt.Errorf("unknown IPFamilyPolicy/s: %s", strIPFamilyPolicy)
}
}
func podNetworkPorts(vmiSpec *v12.VirtualMachineInstanceSpec) []v1.ServicePort {
podNetworkName := ""
for _, network := range vmiSpec.Networks {
if network.Pod != nil {
podNetworkName = network.Name
break
}
}
if podNetworkName != "" {
for _, device := range vmiSpec.Domain.Devices.Interfaces {
if device.Name == podNetworkName {
ports := []v1.ServicePort{}
for i, port := range device.Ports {
ports = append(ports, v1.ServicePort{Name: fmt.Sprintf("port-%d", i+1), Protocol: v1.Protocol(port.Protocol), Port: port.Port})
}
return ports
}
}
}
return nil
}
func serverVersion(virtClient kubecli.KubevirtClient) (major int, minor int, err error) {
serverVersion, err := virtClient.DiscoveryClient().ServerVersion()
if err != nil {
return 0, 0, err
}
// Make a Regex to say we only want numbers
reg, err := regexp.Compile("[^0-9]+")
if err != nil {
return 0, 0, err
}
major, err = strconv.Atoi(reg.ReplaceAllString(serverVersion.Major, ""))
if err != nil {
return 0, 0, err
}
minor, err = strconv.Atoi(reg.ReplaceAllString(serverVersion.Minor, ""))
if err != nil {
return 0, 0, err
}
return
}