Skip to content

Commit 150f8a7

Browse files
author
Roman Sysoev
committed
feat(vm): validate cloud init userdata
Signed-off-by: Roman Sysoev <roman.sysoev@flant.com>
1 parent 508f1e9 commit 150f8a7

5 files changed

Lines changed: 195 additions & 86 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
Copyright 2026 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package service
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
24+
corev1 "k8s.io/api/core/v1"
25+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
26+
"k8s.io/apimachinery/pkg/types"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
29+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
30+
)
31+
32+
const cloudInitUserMaxLen = 2048
33+
34+
type ProvisioningService struct {
35+
reader client.Reader
36+
}
37+
38+
func NewProvisioningService(reader client.Reader) *ProvisioningService {
39+
return &ProvisioningService{
40+
reader: reader,
41+
}
42+
}
43+
44+
var ErrSecretIsNotValid = errors.New("secret is not valid")
45+
46+
type SecretNotFoundError string
47+
48+
func (e SecretNotFoundError) Error() string {
49+
return fmt.Sprintf("secret %s not found", string(e))
50+
}
51+
52+
type UnexpectedSecretTypeError string
53+
54+
func (e UnexpectedSecretTypeError) Error() string {
55+
return fmt.Sprintf("unexpected secret type: %s", string(e))
56+
}
57+
58+
var cloudInitCheckKeys = []string{
59+
"userdata",
60+
"userData",
61+
}
62+
63+
func (p *ProvisioningService) Validate(ctx context.Context, key types.NamespacedName) error {
64+
secret := &corev1.Secret{}
65+
err := p.reader.Get(ctx, key, secret)
66+
if err != nil {
67+
if k8serrors.IsNotFound(err) {
68+
return SecretNotFoundError(key.String())
69+
}
70+
return err
71+
}
72+
switch secret.Type {
73+
case v1alpha2.SecretTypeCloudInit:
74+
return p.validateCloudInitSecret(secret)
75+
case v1alpha2.SecretTypeSysprep:
76+
return p.validateSysprepSecret(secret)
77+
default:
78+
return UnexpectedSecretTypeError(secret.Type)
79+
}
80+
}
81+
82+
func (p *ProvisioningService) validateCloudInitSecret(secret *corev1.Secret) error {
83+
if !p.hasOneOfKeys(secret, cloudInitCheckKeys...) {
84+
return fmt.Errorf("the secret should have one of data fields %v: %w", cloudInitCheckKeys, ErrSecretIsNotValid)
85+
}
86+
return nil
87+
}
88+
89+
func (p *ProvisioningService) validateSysprepSecret(_ *corev1.Secret) error {
90+
return nil
91+
}
92+
93+
func (p *ProvisioningService) hasOneOfKeys(secret *corev1.Secret, checkKeys ...string) bool {
94+
validate := len(checkKeys) == 0
95+
for _, key := range checkKeys {
96+
if _, ok := secret.Data[key]; ok {
97+
validate = true
98+
break
99+
}
100+
}
101+
return validate
102+
}
103+
104+
func (p *ProvisioningService) ValidateUserDataLen(userData string) error {
105+
if userData == "" {
106+
return errors.New("provisioning userdata is defined, but it is empty")
107+
}
108+
109+
if len(userData) > cloudInitUserMaxLen {
110+
return fmt.Errorf("userdata exceeds %d byte limit; should use userDataRef for larger data", cloudInitUserMaxLen)
111+
}
112+
113+
return nil
114+
}

images/virtualization-artifact/pkg/controller/vm/internal/provisioning.go

Lines changed: 14 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import (
2121
"errors"
2222
"fmt"
2323

24-
corev1 "k8s.io/api/core/v1"
25-
k8serrors "k8s.io/apimachinery/pkg/api/errors"
2624
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2725
"k8s.io/apimachinery/pkg/types"
2826
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -38,12 +36,13 @@ import (
3836
const nameProvisioningHandler = "ProvisioningHandler"
3937

4038
func NewProvisioningHandler(client client.Client) *ProvisioningHandler {
41-
return &ProvisioningHandler{client: client, validator: newProvisioningValidator(client)}
39+
return &ProvisioningHandler{
40+
provisioningService: service.NewProvisioningService(client),
41+
}
4242
}
4343

4444
type ProvisioningHandler struct {
45-
client client.Client
46-
validator *provisioningValidator
45+
provisioningService *service.ProvisioningService
4746
}
4847

4948
func (h *ProvisioningHandler) Handle(ctx context.Context, s state.VirtualMachineState) (reconcile.Result, error) {
@@ -73,13 +72,15 @@ func (h *ProvisioningHandler) Handle(ctx context.Context, s state.VirtualMachine
7372
p := current.Spec.Provisioning
7473
switch p.Type {
7574
case v1alpha2.ProvisioningTypeUserData:
76-
if p.UserData != "" {
77-
cb.Status(metav1.ConditionTrue).Reason(vmcondition.ReasonProvisioningReady)
78-
} else {
75+
err := h.provisioningService.ValidateUserDataLen(p.UserData)
76+
if err != nil {
77+
errMsg := fmt.Errorf("failed to validate userdata length: %w", err)
7978
cb.Status(metav1.ConditionFalse).
8079
Reason(vmcondition.ReasonProvisioningNotReady).
81-
Message("Provisioning is defined but it is empty.")
80+
Message(service.CapitalizeFirstLetter(errMsg.Error() + "."))
81+
return reconcile.Result{}, errMsg
8282
}
83+
cb.Status(metav1.ConditionTrue).Reason(vmcondition.ReasonProvisioningReady)
8384
case v1alpha2.ProvisioningTypeUserDataRef:
8485
if p.UserDataRef == nil || p.UserDataRef.Kind != v1alpha2.UserDataRefKindSecret {
8586
cb.Status(metav1.ConditionFalse).
@@ -119,25 +120,25 @@ func (h *ProvisioningHandler) Name() string {
119120
}
120121

121122
func (h *ProvisioningHandler) genConditionFromSecret(ctx context.Context, builder *conditions.ConditionBuilder, secretKey types.NamespacedName) error {
122-
err := h.validator.Validate(ctx, secretKey)
123+
err := h.provisioningService.Validate(ctx, secretKey)
123124

124125
switch {
125126
case err == nil:
126127
builder.Reason(vmcondition.ReasonProvisioningReady).Status(metav1.ConditionTrue)
127128
return nil
128-
case errors.As(err, new(secretNotFoundError)):
129+
case errors.As(err, new(service.SecretNotFoundError)):
129130
builder.Status(metav1.ConditionFalse).
130131
Reason(vmcondition.ReasonProvisioningNotReady).
131132
Message(service.CapitalizeFirstLetter(err.Error()))
132133
return nil
133134

134-
case errors.Is(err, errSecretIsNotValid):
135+
case errors.Is(err, service.ErrSecretIsNotValid):
135136
builder.Status(metav1.ConditionFalse).
136137
Reason(vmcondition.ReasonProvisioningNotReady).
137138
Message(fmt.Sprintf("Invalid secret %q: %s", secretKey.String(), err.Error()))
138139
return nil
139140

140-
case errors.As(err, new(unexpectedSecretTypeError)):
141+
case errors.As(err, new(service.UnexpectedSecretTypeError)):
141142
builder.Status(metav1.ConditionFalse).
142143
Reason(vmcondition.ReasonProvisioningNotReady).
143144
Message(service.CapitalizeFirstLetter(err.Error()))
@@ -147,73 +148,3 @@ func (h *ProvisioningHandler) genConditionFromSecret(ctx context.Context, builde
147148
return err
148149
}
149150
}
150-
151-
var errSecretIsNotValid = errors.New("secret is not valid")
152-
153-
type secretNotFoundError string
154-
155-
func (e secretNotFoundError) Error() string {
156-
return fmt.Sprintf("secret %s not found", string(e))
157-
}
158-
159-
type unexpectedSecretTypeError string
160-
161-
func (e unexpectedSecretTypeError) Error() string {
162-
return fmt.Sprintf("unexpected secret type: %s", string(e))
163-
}
164-
165-
var cloudInitCheckKeys = []string{
166-
"userdata",
167-
"userData",
168-
}
169-
170-
func newProvisioningValidator(reader client.Reader) *provisioningValidator {
171-
return &provisioningValidator{
172-
reader: reader,
173-
}
174-
}
175-
176-
type provisioningValidator struct {
177-
reader client.Reader
178-
}
179-
180-
func (v provisioningValidator) Validate(ctx context.Context, key types.NamespacedName) error {
181-
secret := &corev1.Secret{}
182-
err := v.reader.Get(ctx, key, secret)
183-
if err != nil {
184-
if k8serrors.IsNotFound(err) {
185-
return secretNotFoundError(key.String())
186-
}
187-
return err
188-
}
189-
switch secret.Type {
190-
case v1alpha2.SecretTypeCloudInit:
191-
return v.validateCloudInitSecret(secret)
192-
case v1alpha2.SecretTypeSysprep:
193-
return v.validateSysprepSecret(secret)
194-
default:
195-
return unexpectedSecretTypeError(secret.Type)
196-
}
197-
}
198-
199-
func (v provisioningValidator) validateCloudInitSecret(secret *corev1.Secret) error {
200-
if !v.hasOneOfKeys(secret, cloudInitCheckKeys...) {
201-
return fmt.Errorf("the secret should have one of data fields %v: %w", cloudInitCheckKeys, errSecretIsNotValid)
202-
}
203-
return nil
204-
}
205-
206-
func (v provisioningValidator) validateSysprepSecret(_ *corev1.Secret) error {
207-
return nil
208-
}
209-
210-
func (v provisioningValidator) hasOneOfKeys(secret *corev1.Secret, checkKeys ...string) bool {
211-
validate := len(checkKeys) == 0
212-
for _, key := range checkKeys {
213-
if _, ok := secret.Data[key]; ok {
214-
validate = true
215-
break
216-
}
217-
}
218-
return validate
219-
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2026 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package validators
18+
19+
import (
20+
"context"
21+
22+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
23+
24+
"github.com/deckhouse/virtualization-controller/pkg/controller/service"
25+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
26+
)
27+
28+
type ProvisioningValidator struct {
29+
provisioningService *service.ProvisioningService
30+
}
31+
32+
func NewProvisioningValidator(provisioningService *service.ProvisioningService) *ProvisioningValidator {
33+
return &ProvisioningValidator{provisioningService: provisioningService}
34+
}
35+
36+
func (p *ProvisioningValidator) ValidateCreate(_ context.Context, vm *v1alpha2.VirtualMachine) (admission.Warnings, error) {
37+
err := p.validateUserDataLen(vm)
38+
if err != nil {
39+
return admission.Warnings{}, err
40+
}
41+
42+
return nil, nil
43+
}
44+
45+
func (p *ProvisioningValidator) ValidateUpdate(_ context.Context, _, newVM *v1alpha2.VirtualMachine) (admission.Warnings, error) {
46+
err := p.validateUserDataLen(newVM)
47+
if err != nil {
48+
return admission.Warnings{}, err
49+
}
50+
51+
return nil, nil
52+
}
53+
54+
func (p *ProvisioningValidator) validateUserDataLen(vm *v1alpha2.VirtualMachine) error {
55+
if vm.Spec.Provisioning != nil && vm.Spec.Provisioning.Type == v1alpha2.ProvisioningTypeUserData {
56+
err := p.provisioningService.ValidateUserDataLen(vm.Spec.Provisioning.UserData)
57+
if err != nil {
58+
return err
59+
}
60+
}
61+
return nil
62+
}

images/virtualization-artifact/pkg/controller/vm/vm_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func SetupController(
5454
mgrCache := mgr.GetCache()
5555
client := mgr.GetClient()
5656
blockDeviceService := service.NewBlockDeviceService(client)
57+
provisioningService := service.NewProvisioningService(client)
5758
vmClassService := service.NewVirtualMachineClassService(client)
5859

5960
migrateVolumesService := vmservice.NewMigrationVolumesService(client, internal.MakeKVVMFromVMSpec, 10*time.Second)
@@ -100,7 +101,7 @@ func SetupController(
100101

101102
if err = builder.WebhookManagedBy(mgr).
102103
For(&v1alpha2.VirtualMachine{}).
103-
WithValidator(NewValidator(client, blockDeviceService, featuregates.Default(), log)).
104+
WithValidator(NewValidator(client, blockDeviceService, provisioningService, featuregates.Default(), log)).
104105
WithDefaulter(NewDefaulter(client, vmClassService, log)).
105106
Complete(); err != nil {
106107
return err

images/virtualization-artifact/pkg/controller/vm/vm_webhook.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,20 @@ type Validator struct {
4242
log *log.Logger
4343
}
4444

45-
func NewValidator(client client.Client, service *service.BlockDeviceService, featureGate featuregate.FeatureGate, log *log.Logger) *Validator {
45+
func NewValidator(client client.Client, blockDeviceservice *service.BlockDeviceService, provisioningService *service.ProvisioningService, featureGate featuregate.FeatureGate, log *log.Logger) *Validator {
4646
return &Validator{
4747
validators: []VirtualMachineValidator{
4848
validators.NewMetaValidator(client),
4949
validators.NewIPAMValidator(client),
5050
validators.NewBlockDeviceSpecRefsValidator(),
5151
validators.NewSizingPolicyValidator(client),
52-
validators.NewBlockDeviceLimiterValidator(service, log),
52+
validators.NewBlockDeviceLimiterValidator(blockDeviceservice, log),
5353
validators.NewAffinityValidator(),
5454
validators.NewTopologySpreadConstraintValidator(),
5555
validators.NewCPUCountValidator(),
5656
validators.NewNetworksValidator(featureGate),
5757
validators.NewFirstDiskValidator(client),
58+
validators.NewProvisioningValidator(provisioningService),
5859
},
5960
log: log.With("webhook", "validation"),
6061
}

0 commit comments

Comments
 (0)