Skip to content

Commit d5074c3

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 d5074c3

5 files changed

Lines changed: 193 additions & 86 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
25+
corev1 "k8s.io/api/core/v1"
26+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
27+
"k8s.io/apimachinery/pkg/types"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
)
30+
31+
const cloudInitUserMaxLen = 2048
32+
33+
type ProvisioningService struct {
34+
reader client.Reader
35+
}
36+
37+
func NewProvisioningService(reader client.Reader) *ProvisioningService {
38+
return &ProvisioningService{
39+
reader: reader,
40+
}
41+
}
42+
43+
var ErrSecretIsNotValid = errors.New("secret is not valid")
44+
45+
type SecretNotFoundError string
46+
47+
func (e SecretNotFoundError) Error() string {
48+
return fmt.Sprintf("secret %s not found", string(e))
49+
}
50+
51+
type UnexpectedSecretTypeError string
52+
53+
func (e UnexpectedSecretTypeError) Error() string {
54+
return fmt.Sprintf("unexpected secret type: %s", string(e))
55+
}
56+
57+
var cloudInitCheckKeys = []string{
58+
"userdata",
59+
"userData",
60+
}
61+
62+
func (p *ProvisioningService) Validate(ctx context.Context, key types.NamespacedName) error {
63+
secret := &corev1.Secret{}
64+
err := p.reader.Get(ctx, key, secret)
65+
if err != nil {
66+
if k8serrors.IsNotFound(err) {
67+
return SecretNotFoundError(key.String())
68+
}
69+
return err
70+
}
71+
switch secret.Type {
72+
case v1alpha2.SecretTypeCloudInit:
73+
return p.validateCloudInitSecret(secret)
74+
case v1alpha2.SecretTypeSysprep:
75+
return p.validateSysprepSecret(secret)
76+
default:
77+
return UnexpectedSecretTypeError(secret.Type)
78+
}
79+
}
80+
81+
func (p *ProvisioningService) validateCloudInitSecret(secret *corev1.Secret) error {
82+
if !p.hasOneOfKeys(secret, cloudInitCheckKeys...) {
83+
return fmt.Errorf("the secret should have one of data fields %v: %w", cloudInitCheckKeys, ErrSecretIsNotValid)
84+
}
85+
return nil
86+
}
87+
88+
func (v *ProvisioningService) validateSysprepSecret(_ *corev1.Secret) error {
89+
return nil
90+
}
91+
92+
func (v *ProvisioningService) hasOneOfKeys(secret *corev1.Secret, checkKeys ...string) bool {
93+
validate := len(checkKeys) == 0
94+
for _, key := range checkKeys {
95+
if _, ok := secret.Data[key]; ok {
96+
validate = true
97+
break
98+
}
99+
}
100+
return validate
101+
}
102+
103+
func (p *ProvisioningService) ValidateUserDataLen(userData string) error {
104+
if userData == "" {
105+
return errors.New("provisioning userdata is defined, but it is empty")
106+
}
107+
108+
if len(userData) > cloudInitUserMaxLen {
109+
return fmt.Errorf("userdata exceeds %d byte limit; should use userDataRef for larger data", cloudInitUserMaxLen)
110+
}
111+
112+
return nil
113+
}

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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
"github.com/deckhouse/virtualization-controller/pkg/controller/service"
23+
"github.com/deckhouse/virtualization/api/core/v1alpha2"
24+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
25+
)
26+
27+
type ProvisioningValidator struct {
28+
provisioningService *service.ProvisioningService
29+
}
30+
31+
func NewProvisioningValidator(provisioningService *service.ProvisioningService) *ProvisioningValidator {
32+
return &ProvisioningValidator{provisioningService: provisioningService}
33+
}
34+
35+
func (p *ProvisioningValidator) ValidateCreate(_ context.Context, vm *v1alpha2.VirtualMachine) (admission.Warnings, error) {
36+
err := p.validateUserDataLen(vm)
37+
if err != nil {
38+
return admission.Warnings{}, err
39+
}
40+
41+
return nil, nil
42+
}
43+
44+
func (p *ProvisioningValidator) ValidateUpdate(_ context.Context, _, newVM *v1alpha2.VirtualMachine) (admission.Warnings, error) {
45+
err := p.validateUserDataLen(newVM)
46+
if err != nil {
47+
return admission.Warnings{}, err
48+
}
49+
50+
return nil, nil
51+
}
52+
53+
func (p *ProvisioningValidator) validateUserDataLen(vm *v1alpha2.VirtualMachine) error {
54+
if vm.Spec.Provisioning != nil && vm.Spec.Provisioning.Type == v1alpha2.ProvisioningTypeUserData {
55+
err := p.provisioningService.ValidateUserDataLen(vm.Spec.Provisioning.UserData)
56+
if err != nil {
57+
return err
58+
}
59+
}
60+
return nil
61+
}

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)