Skip to content

Commit

Permalink
Validate CloudConfig using schema
Browse files Browse the repository at this point in the history
This patch ensures the CloudConfig YAML produced by the inline
CloudInit spec is validated against the official CloudConfig
schema from Cloud-Init.

This patch also relocates the `cloudinit` utility package from
`./pkg/vmprovider/providers/vsphere2/cloudinit` to
`./pkg/util/cloudinit`. Since we do no anticipate future API
revisions as a _huge_ departure from what has been established
in v1alpha2, it is safe to use those types in utility code as
they can be find/replaced with later schema versions,
ex. `v1alpha3`, when that time comes. Other than the reference
to a `github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit.CloudConfig`,
the code in `./pkg/util/cloudinit` is completely unrelated to
the vSphere provider, hence its relocation under the utility
package.
  • Loading branch information
akutz committed Dec 14, 2023
1 parent 5477585 commit 0a934e7
Show file tree
Hide file tree
Showing 22 changed files with 7,391 additions and 25 deletions.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ run:
- external/
- controllers/virtualmachineservice/v1alpha1/utils/
- controllers/virtualmachineservice/v1alpha2/utils/
- pkg/util/cloudinit/schema/

# override defaults
linters-settings:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ package cloudinit
import (
"bytes"
"context"
"fmt"
"strconv"

"gopkg.in/yaml.v3"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit"
"github.com/vmware-tanzu/vm-operator/api/v1alpha2/common"

"github.com/vmware-tanzu/vm-operator/pkg/util/cloudinit/validate"
)

// cloudConfig provides support for marshalling the object to a valid
Expand Down Expand Up @@ -45,7 +47,7 @@ type user struct {
Groups []string `json:"groups,omitempty" yaml:"groups,omitempty"`
HashedPasswd string `json:"hashed_passwd,omitempty" yaml:"hashed_passwd,omitempty"`
Homedir *string `json:"homedir,omitempty" yaml:"homedir,omitempty"`
Inactive *int32 `json:"inactive,omitempty" yaml:"inactive,omitempty"`
Inactive *string `json:"inactive,omitempty" yaml:"inactive,omitempty"`
LockPasswd *bool `json:"lock_passwd,omitempty" yaml:"lock_passwd,omitempty"`
Name string `json:"name" yaml:"name"`
NoCreateHome *bool `json:"no_create_home,omitempty" yaml:"no_create_home,omitempty"`
Expand Down Expand Up @@ -76,8 +78,9 @@ type writeFile struct {

const emptyYAMLObject = "{}\n"

// MarshalYAML marshals the provided CloudConfig and secret data to a valid,
// YAML CloudConfig document.
func MarshalYAML(
ctx context.Context,
in cloudinit.CloudConfig,
secret CloudConfigSecretData) (string, error) {

Expand Down Expand Up @@ -119,11 +122,24 @@ func MarshalYAML(

if l := len(in.WriteFiles); l > 0 {
out.WriteFiles = make([]writeFile, l)

for i := range in.WriteFiles {

// If the content was not derived from a secret, then get it as
// a string from the Content field.
content := secret.WriteFiles[in.WriteFiles[i].Path]
if content == "" {
if err := yaml.Unmarshal(
in.WriteFiles[i].Content, &content); err != nil {

return "", err
}
}

copyWriteFile(
in.WriteFiles[i],
&out.WriteFiles[i],
secret.WriteFiles[in.WriteFiles[i].Path])
content)
}
}

Expand All @@ -139,6 +155,11 @@ func MarshalYAML(
return "", nil
}

// Validate the produced CloudConfig YAML using the CloudConfig schema.
if err := validate.CloudConfigYAML(data); err != nil {
return "", err
}

return data, nil
}

Expand All @@ -158,7 +179,12 @@ func copyUser(

out.HashedPasswd = secret.HashPasswd
out.Homedir = in.Homedir
out.Inactive = in.Inactive

if v := in.Inactive; v != nil {
s := strconv.Itoa(int(*v))
out.Inactive = &s
}

out.LockPasswd = in.LockPasswd
out.Name = in.Name
out.NoCreateHome = in.NoCreateHome
Expand Down Expand Up @@ -216,10 +242,8 @@ func (ccu *cloudConfigUsers) MarshalYAML() (any, error) {

func (ccu cloudConfigRunCmd) MarshalYAML() (any, error) {
if ccu.singleString != "" {
fmt.Printf("singleStringVal=%s\n", ccu.singleString)
return ccu.singleString, nil
} else if len(ccu.listOfStrings) > 0 {
fmt.Printf("listOfStrings=%v\n", ccu.listOfStrings)
return ccu.listOfStrings, nil
}
return nil, nil
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

vmopv1cloudinit "github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit"
"github.com/vmware-tanzu/vm-operator/api/v1alpha2/common"
"github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/cloudinit"
"github.com/vmware-tanzu/vm-operator/pkg/util/cloudinit"
)

var _ = Describe("CloudConfig GetCloudConfigSecretData", func() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,26 @@ import (

vmopv1cloudinit "github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit"
"github.com/vmware-tanzu/vm-operator/api/v1alpha2/common"
"github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/cloudinit"
"github.com/vmware-tanzu/vm-operator/pkg/util/cloudinit"
)

var _ = Describe("CloudConfig MarshalYAML", func() {
var (
err error
data string
ctx context.Context
cloudConfig vmopv1cloudinit.CloudConfig
cloudConfigSecretData cloudinit.CloudConfigSecretData
)

BeforeEach(func() {
err = nil
data = ""
ctx = context.Background()
cloudConfig = vmopv1cloudinit.CloudConfig{}
cloudConfigSecretData = cloudinit.CloudConfigSecretData{}
})

JustBeforeEach(func() {
data, err = cloudinit.MarshalYAML(ctx, cloudConfig, cloudConfigSecretData)
data, err = cloudinit.MarshalYAML(cloudConfig, cloudConfigSecretData)
})

When("CloudConfig and CloudConfigSecretData are both empty", func() {
Expand Down Expand Up @@ -159,6 +157,118 @@ var _ = Describe("CloudConfig MarshalYAML", func() {
})
})

When("CloudConfig has all possible fields set", func() {
BeforeEach(func() {
cloudConfig = vmopv1cloudinit.CloudConfig{
Users: []vmopv1cloudinit.User{
{
Name: "bob.wilson",
CreateGroups: addrOf(false),
ExpireDate: addrOf("9999-99-99"),
Gecos: addrOf("gecos"),
Groups: []string{"group1", "group2"},
HashedPasswd: &common.SecretKeySelector{
Name: "my-bootstrap-data",
Key: "cloud-init-user-bob.wilson-hashed_passwd",
},
Homedir: addrOf("/home/bob.wilson"),
Inactive: addrOf(int32(1)),
LockPasswd: addrOf(false),
NoCreateHome: addrOf(false),
NoLogInit: addrOf(false),
PrimaryGroup: addrOf("group1"),
SELinuxUser: addrOf("bob.wilson"),
Shell: addrOf("/bin/bash"),
SnapUser: addrOf("bob.wilson"),
SSHAuthorizedKeys: []string{"key1", "key2"},
SSHImportID: []string{"id1", "id2"},
SSHRedirectUser: addrOf(false),
Sudo: addrOf("sudoyou?"),
System: addrOf(false),
UID: addrOf(int64(123)),
},
{
Name: "rob.wilson",
CreateGroups: addrOf(true),
ExpireDate: addrOf("9999-99-99"),
Gecos: addrOf("gecos"),
Groups: []string{"group1", "group2"},
Homedir: addrOf("/home/rob.wilson"),
Inactive: addrOf(int32(10)),
LockPasswd: addrOf(true),
NoCreateHome: addrOf(true),
NoLogInit: addrOf(true),
Passwd: &common.SecretKeySelector{
Name: "my-bootstrap-data",
Key: "cloud-init-user-rob.wilson-passwd",
},
PrimaryGroup: addrOf("group1"),
SELinuxUser: addrOf("rob.wilson"),
Shell: addrOf("/bin/bash"),
SnapUser: addrOf("rob.wilson"),
SSHAuthorizedKeys: []string{"key1", "key2"},
SSHImportID: []string{"id1", "id2"},
SSHRedirectUser: addrOf(true),
Sudo: addrOf("sudoyou?"),
System: addrOf(true),
UID: addrOf(int64(123)),
},
},
RunCmd: []json.RawMessage{
[]byte("ls /"),
[]byte(`[ "ls", "-a", "-l", "/" ]`),
[]byte("- echo\n- \"hello, world.\""),
},
WriteFiles: []vmopv1cloudinit.WriteFile{
{
Path: "/hello",
Content: []byte("world"),
Append: true,
Defer: true,
Encoding: vmopv1cloudinit.WriteFileEncodingTextPlain,
Owner: "bob.wilson:bob.wilson",
Permissions: "0644",
},
{
Path: "/hi",
Content: []byte("name: \"my-bootstrap-data\"\nkey: \"/hi\""),
Append: false,
Defer: false,
Encoding: vmopv1cloudinit.WriteFileEncodingTextPlain,
Owner: "rob.wilson:rob.wilson",
Permissions: "0755",
},
{
Path: "/doc",
Content: []byte("|\n a multi-line\n document"),
Append: true,
Defer: true,
Encoding: vmopv1cloudinit.WriteFileEncodingTextPlain,
Owner: "bob.wilson:bob.wilson",
Permissions: "0644",
},
},
}
cloudConfigSecretData = cloudinit.CloudConfigSecretData{
Users: map[string]cloudinit.CloudConfigUserSecretData{
"bob.wilson": {
HashPasswd: "0123456789",
},
"rob.wilson": {
Passwd: "password",
},
},
WriteFiles: map[string]string{
"/hi": "there",
},
}
})
It("Should return user data", func() {
Expect(err).ToNot(HaveOccurred())
Expect(data).To(Equal(cloudConfigWithAllPossibleValues))
})
})

})

var _ = Describe("CloudConfig GetSecretResources", func() {
Expand Down Expand Up @@ -348,3 +458,90 @@ runcmd:
- - echo
- hello, world.
`

const cloudConfigWithAllPossibleValues = `users:
- create_groups: false
expiredate: 9999-99-99
gecos: gecos
groups:
- group1
- group2
hashed_passwd: "0123456789"
homedir: /home/bob.wilson
inactive: "1"
lock_passwd: false
name: bob.wilson
no_create_home: false
no_log_init: false
primary_group: group1
selinux_user: bob.wilson
shell: /bin/bash
snapuser: bob.wilson
ssh_authorized_keys:
- key1
- key2
ssh_import_id:
- id1
- id2
ssh_redirect_user: false
sudo: sudoyou?
system: false
uid: 123
- create_groups: true
expiredate: 9999-99-99
gecos: gecos
groups:
- group1
- group2
homedir: /home/rob.wilson
inactive: "10"
lock_passwd: true
name: rob.wilson
no_create_home: true
no_log_init: true
passwd: password
primary_group: group1
selinux_user: rob.wilson
shell: /bin/bash
snapuser: rob.wilson
ssh_authorized_keys:
- key1
- key2
ssh_import_id:
- id1
- id2
ssh_redirect_user: true
sudo: sudoyou?
system: true
uid: 123
runcmd:
- ls /
- - ls
- -a
- -l
- /
- - echo
- hello, world.
write_files:
- append: true
content: world
defer: true
encoding: text/plain
owner: bob.wilson:bob.wilson
path: /hello
permissions: "0644"
- content: there
encoding: text/plain
owner: rob.wilson:rob.wilson
path: /hi
permissions: "0755"
- append: true
content: |-
a multi-line
document
defer: true
encoding: text/plain
owner: bob.wilson:bob.wilson
path: /doc
permissions: "0644"
`
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ func TestCloudInit(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "vSphere Provider Cloud-Init Suite")
}

func addrOf[T any](t T) *T {
return &t
}
17 changes: 17 additions & 0 deletions pkg/util/cloudinit/schema/Dockerfile.quicktype
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM node:20 AS build
WORKDIR /quicktype

RUN npm install --prefix /quicktype quicktype

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /quicktype /quicktype

WORKDIR /output
CMD [ \
"/quicktype/node_modules/quicktype/dist/index.js", \
"--src", "/schema.json", \
"--src-lang", "schema", \
"--out", "/output/cloudconfig.go", \
"--lang", "go", \
"--package", "schema" \
]
Loading

0 comments on commit 0a934e7

Please sign in to comment.