Skip to content

Commit a737b08

Browse files
feat(virtualization-api): first implementation (#11)
Description Added a new component virtualization-api - extension API server. Why do we need it, and what problem does it solve? Using the api server, we can connect to virtual machines and perform various operations. Currently implemented: console, vnc, port forwarding --------- Signed-off-by: Yaroslav Borbat <[email protected]> Signed-off-by: Yaroslav Borbat <[email protected]> Co-authored-by: Ivan Mikheykin <[email protected]>
1 parent 90c7a77 commit a737b08

File tree

190 files changed

+38081
-243
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

190 files changed

+38081
-243
lines changed

crds/virtualmachine.yaml

+48
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,18 @@ spec:
378378
topologyKey:
379379
type: string
380380
description: ""
381+
matchLabelKeys:
382+
items:
383+
type: string
384+
description: ""
385+
type: array
386+
description: ""
387+
mismatchLabelKeys:
388+
items:
389+
type: string
390+
description: ""
391+
type: array
392+
description: ""
381393
required:
382394
- topologyKey
383395
type: object
@@ -471,6 +483,18 @@ spec:
471483
topologyKey:
472484
type: string
473485
description: ""
486+
matchLabelKeys:
487+
items:
488+
type: string
489+
description: ""
490+
type: array
491+
description: ""
492+
mismatchLabelKeys:
493+
items:
494+
type: string
495+
description: ""
496+
type: array
497+
description: ""
474498
required:
475499
- topologyKey
476500
type: object
@@ -559,6 +583,18 @@ spec:
559583
topologyKey:
560584
type: string
561585
description: ""
586+
matchLabelKeys:
587+
items:
588+
type: string
589+
description: ""
590+
type: array
591+
description: ""
592+
mismatchLabelKeys:
593+
items:
594+
type: string
595+
description: ""
596+
type: array
597+
description: ""
562598
required:
563599
- topologyKey
564600
type: object
@@ -652,6 +688,18 @@ spec:
652688
topologyKey:
653689
type: string
654690
description: ""
691+
matchLabelKeys:
692+
items:
693+
type: string
694+
description: ""
695+
type: array
696+
description: ""
697+
mismatchLabelKeys:
698+
items:
699+
type: string
700+
description: ""
701+
type: array
702+
description: ""
655703
required:
656704
- topologyKey
657705
type: object

hooks/generate_certificates.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# limitations under the License.
1616

1717

18-
from lib.hooks.internal_tls import GenerateCertificatesHook, CertitifacteRequest, CACertitifacteRequest, default_sans
18+
from lib.hooks.internal_tls import *
1919
from lib.module import values as module_values
2020
from deckhouse.hook import Context
2121
from typing import Callable
@@ -25,7 +25,7 @@
2525
def main():
2626
hook = GenerateCertificatesHook(
2727
CertitifacteRequest(
28-
cn=f"virtualization-controller",
28+
cn="virtualization-controller",
2929
sansGenerator=default_sans([
3030
"virtualization-controller-admission-webhook",
3131
f"virtualization-controller-admission-webhook.{common.NAMESPACE}",
@@ -46,11 +46,30 @@ def main():
4646
before_gen_check=dvcr_before_check
4747
),
4848

49+
CertitifacteRequest(
50+
cn="virtualization-api",
51+
sansGenerator=default_sans([
52+
"virtualization-api",
53+
f"virtualization-api.{common.NAMESPACE}",
54+
f"virtualization-api.{common.NAMESPACE}.svc"],
55+
),
56+
tls_secret_name="virtualization-api-tls",
57+
values_path_prefix=f"{common.MODULE_NAME}.internal.apiserver.cert"
58+
),
59+
60+
CertitifacteRequest(
61+
cn="virtualization-api-proxy",
62+
sansGenerator=empty_sans(),
63+
tls_secret_name="virtualization-api-proxy-tls",
64+
values_path_prefix=f"{common.MODULE_NAME}.internal.apiserver.proxyCert",
65+
extended_key_usages = [EXTENDED_KEY_USAGES[1]]
66+
),
67+
4968
namespace=common.NAMESPACE,
5069
module_name=common.MODULE_NAME,
5170

5271
ca_request=CACertitifacteRequest(
53-
cn=f"virtualization.deckhouse.io",
72+
cn="virtualization.deckhouse.io",
5473
ca_secret_name="virtualization-ca",
5574
values_path_prefix=f"{common.MODULE_NAME}.internal.rootCA",
5675
))

hooks/lib/certificate/certificate.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ def with_hosts(self, *hosts: str):
170170
if not is_valid_hostname(h):
171171
continue
172172
alt_names.append(f"DNS:{h}")
173-
self.add_extension("subjectAltName", False, ", ".join(alt_names))
173+
if len(alt_names) > 0:
174+
self.add_extension("subjectAltName", False, ", ".join(alt_names))
174175
return self
175176

176177
def __sign(self, ca_subj: crypto.X509Name, ca_key: crypto.PKey) -> None:

hooks/lib/certificate/parse.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@ def parse_key(key: str) -> crypto.PKey:
2727

2828

2929
def get_certificate_san(crt: crypto.X509) -> list[str]:
30-
san = ''
30+
san = ""
3131
ext_count = crt.get_extension_count()
3232
for i in range(0, ext_count):
3333
ext = crt.get_extension(i)
34-
if 'subjectAltName' in str(ext.get_short_name()):
34+
if "subjectAltName" in str(ext.get_short_name()):
3535
san = ext.__str__()
36-
return san.split(', ')
36+
break
37+
if len(san) > 0:
38+
return san.split(', ')
39+
return []
40+
3741

3842

3943
def is_outdated_ca(ca: str, cert_outdated_duration: timedelta) -> bool:

hooks/lib/hooks/internal_tls.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def r(ctx: hook.Context):
306306
snaps[s["filterResult"]["name"]] = TLSSecretData(
307307
s["filterResult"]["data"])
308308
ca_data = TLSSecretData()
309-
if self.__with_common_ca:
309+
if self.__with_common_ca():
310310
tls_value_data = self.__sync_ca(self.ca_request,
311311
snaps.get(self.ca_request.ca_secret_name, TLSSecretData()))
312312
ca_data = convert_to_TLSSecretData(tls_value_data)
@@ -456,3 +456,9 @@ def generate_sans(ctx: hook.Context) -> list[str]:
456456
res.append(san)
457457
return res
458458
return generate_sans
459+
460+
461+
def empty_sans() -> Callable[[hook.Context], list[str]]:
462+
def generator(ctx: hook.Context) -> list:
463+
return []
464+
return generator
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
diff --git a/pkg/controller/virtinformers.go b/pkg/controller/virtinformers.go
2+
index 5cbb8197f..82f6f9238 100644
3+
--- a/pkg/controller/virtinformers.go
4+
+++ b/pkg/controller/virtinformers.go
5+
@@ -300,6 +300,8 @@ type KubeInformerFactory interface {
6+
ResourceQuota() cache.SharedIndexInformer
7+
8+
K8SInformerFactory() informers.SharedInformerFactory
9+
+
10+
+ VirtualizationCA() cache.SharedIndexInformer
11+
}
12+
13+
type kubeInformerFactory struct {
14+
@@ -1293,3 +1295,12 @@ func VolumeSnapshotClassInformer(clientSet kubecli.KubevirtClient, resyncPeriod
15+
lw := cache.NewListWatchFromClient(restClient, "volumesnapshotclasses", k8sv1.NamespaceAll, fields.Everything())
16+
return cache.NewSharedIndexInformer(lw, &vsv1.VolumeSnapshotClass{}, resyncPeriod, cache.Indexers{})
17+
}
18+
+
19+
+func (f *kubeInformerFactory) VirtualizationCA() cache.SharedIndexInformer {
20+
+ return f.getInformer("extensionsVirtualizationCAConfigMapInformer", func() cache.SharedIndexInformer {
21+
+ restClient := f.clientSet.CoreV1().RESTClient()
22+
+ fieldSelector := fields.OneTermEqualSelector("metadata.name", "virtualization-ca")
23+
+ lw := cache.NewListWatchFromClient(restClient, "configmaps", f.kubevirtNamespace, fieldSelector)
24+
+ return cache.NewSharedIndexInformer(lw, &k8sv1.ConfigMap{}, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
25+
+ })
26+
+}
27+
diff --git a/pkg/util/tls/tls.go b/pkg/util/tls/tls.go
28+
index e9e140548..e2a349012 100644
29+
--- a/pkg/util/tls/tls.go
30+
+++ b/pkg/util/tls/tls.go
31+
@@ -132,6 +132,55 @@ func SetupTLSWithCertManager(caManager ClientCAManager, certManager certificate.
32+
return tlsConfig
33+
}
34+
35+
+func SetupTLSWithVirtualizationCAManager(caManager, virtualizationCAManager ClientCAManager, certManager certificate.Manager, clientAuth tls.ClientAuthType, clusterConfig *virtconfig.ClusterConfig) *tls.Config {
36+
+ tlsConfig := &tls.Config{
37+
+ GetCertificate: func(info *tls.ClientHelloInfo) (certificate *tls.Certificate, err error) {
38+
+ cert := certManager.Current()
39+
+ if cert == nil {
40+
+ return nil, fmt.Errorf(noSrvCertMessage)
41+
+ }
42+
+ return cert, nil
43+
+ },
44+
+ GetConfigForClient: func(hi *tls.ClientHelloInfo) (*tls.Config, error) {
45+
+ cert := certManager.Current()
46+
+ if cert == nil {
47+
+ return nil, fmt.Errorf(noSrvCertMessage)
48+
+ }
49+
+
50+
+ clientCAPool, err := caManager.GetCurrent()
51+
+ if err != nil {
52+
+ log.Log.Reason(err).Error("Failed to get requestheader client CA")
53+
+ return nil, err
54+
+ }
55+
+
56+
+ virtualizationCA, err := virtualizationCAManager.GetCurrentRaw()
57+
+ if err != nil {
58+
+ log.Log.Reason(err).Error("Failed to get CA from config-map virtualization-ca")
59+
+ return nil, err
60+
+ }
61+
+
62+
+ clientCAPool.AppendCertsFromPEM(virtualizationCA)
63+
+
64+
+ kv := clusterConfig.GetConfigFromKubeVirtCR()
65+
+ tlsConfig := getTLSConfiguration(kv)
66+
+ ciphers := CipherSuiteIds(tlsConfig.Ciphers)
67+
+ minTLSVersion := TLSVersion(tlsConfig.MinTLSVersion)
68+
+ config := &tls.Config{
69+
+ CipherSuites: ciphers,
70+
+ MinVersion: minTLSVersion,
71+
+ Certificates: []tls.Certificate{*cert},
72+
+ ClientCAs: clientCAPool,
73+
+ ClientAuth: clientAuth,
74+
+ }
75+
+
76+
+ config.BuildNameToCertificate()
77+
+ return config, nil
78+
+ },
79+
+ }
80+
+ tlsConfig.BuildNameToCertificate()
81+
+ return tlsConfig
82+
+}
83+
+
84+
func SetupTLSForVirtHandlerServer(caManager ClientCAManager, certManager certificate.Manager, externallyManaged bool, clusterConfig *virtconfig.ClusterConfig) *tls.Config {
85+
// #nosec cause: InsecureSkipVerify: true
86+
// resolution: Neither the client nor the server should validate anything itself, `VerifyPeerCertificate` is still executed
87+
diff --git a/pkg/virt-api/api.go b/pkg/virt-api/api.go
88+
index 120f2d68f..4b82edd13 100644
89+
--- a/pkg/virt-api/api.go
90+
+++ b/pkg/virt-api/api.go
91+
@@ -884,7 +884,7 @@ func (app *virtAPIApp) registerMutatingWebhook(informers *webhooks.Informers) {
92+
})
93+
}
94+
95+
-func (app *virtAPIApp) setupTLS(k8sCAManager kvtls.ClientCAManager, kubevirtCAManager kvtls.ClientCAManager) {
96+
+func (app *virtAPIApp) setupTLS(k8sCAManager, kubevirtCAManager, virtualizationCAManager kvtls.ClientCAManager) {
97+
98+
// A VerifyClientCertIfGiven request means we're not guaranteed
99+
// a client has been authenticated unless they provide a peer
100+
@@ -901,7 +901,7 @@ func (app *virtAPIApp) setupTLS(k8sCAManager kvtls.ClientCAManager, kubevirtCAMa
101+
// response is given. That status request won't send a peer cert regardless
102+
// if the TLS handshake requests it. As a result, the TLS handshake fails
103+
// and our aggregated endpoint never becomes available.
104+
- app.tlsConfig = kvtls.SetupTLSWithCertManager(k8sCAManager, app.certmanager, tls.VerifyClientCertIfGiven, app.clusterConfig)
105+
+ app.tlsConfig = kvtls.SetupTLSWithVirtualizationCAManager(k8sCAManager, virtualizationCAManager, app.certmanager, tls.VerifyClientCertIfGiven, app.clusterConfig)
106+
app.handlerTLSConfiguration = kvtls.SetupTLSForVirtHandlerClients(kubevirtCAManager, app.handlerCertManager, app.externallyManaged)
107+
}
108+
109+
@@ -919,10 +919,12 @@ func (app *virtAPIApp) startTLS(informerFactory controller.KubeInformerFactory)
110+
111+
authConfigMapInformer := informerFactory.ApiAuthConfigMap()
112+
kubevirtCAConfigInformer := informerFactory.KubeVirtCAConfigMap()
113+
+ virtualizationCAConfigInformer := informerFactory.VirtualizationCA()
114+
115+
k8sCAManager := kvtls.NewKubernetesClientCAManager(authConfigMapInformer.GetStore())
116+
kubevirtCAInformer := kvtls.NewCAManager(kubevirtCAConfigInformer.GetStore(), app.namespace, app.caConfigMapName)
117+
- app.setupTLS(k8sCAManager, kubevirtCAInformer)
118+
+ virtualizationCAInformer := kvtls.NewCAManager(virtualizationCAConfigInformer.GetStore(), app.namespace, "virtualization-ca")
119+
+ app.setupTLS(k8sCAManager, kubevirtCAInformer, virtualizationCAInformer)
120+
121+
app.Compose()
122+
123+
@@ -1007,6 +1009,7 @@ func (app *virtAPIApp) Run() {
124+
125+
kubeInformerFactory.ApiAuthConfigMap()
126+
kubeInformerFactory.KubeVirtCAConfigMap()
127+
+ kubeInformerFactory.VirtualizationCA()
128+
crdInformer := kubeInformerFactory.CRD()
129+
vmiPresetInformer := kubeInformerFactory.VirtualMachinePreset()
130+
vmRestoreInformer := kubeInformerFactory.VirtualMachineRestore()

images/virt-artifact/patches/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@ Rename group name for all kubevirt CRDs to override them with deckhouse virtuali
3232

3333
Also, remove short names and change categories. Just in case.
3434

35+
#### `011-virt-api-authentication.patch`
36+
Added the ability for virt-api to authenticate clients with certificates signed by our rootCA located in the config-map virtualization-ca.
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
image: {{ $.ImageName }}
3+
fromImage: base-ubuntu-jammy
4+
import:
5+
- image: virtualization-artifact
6+
add: /usr/local/go/src/virtualization-controller/virtualization-api
7+
to: /app/virtualization-api
8+
after: install
9+
docker:
10+
USER: "65532:65532"
11+
WORKDIR: "/app"
12+
ENTRYPOINT: ["/app/virtualization-api"]

0 commit comments

Comments
 (0)