diff --git a/api/v1alpha1/clusterconfig_types.go b/api/v1alpha1/clusterconfig_types.go index 17e5b119e..a167df0d7 100644 --- a/api/v1alpha1/clusterconfig_types.go +++ b/api/v1alpha1/clusterconfig_types.go @@ -98,6 +98,9 @@ type GenericClusterConfig struct { // +optional Addons *Addons `json:"addons,omitempty"` + + // +optional + Users Users `json:"users,omitempty"` } func (s GenericClusterConfig) VariableSchema() clusterv1.VariableSchema { //nolint:gocritic,lll // Passed by value for no potential side-effect. @@ -116,6 +119,7 @@ func (s GenericClusterConfig) VariableSchema() clusterv1.VariableSchema { //noli OpenAPIV3Schema, "imageRegistries": ImageRegistries{}.VariableSchema().OpenAPIV3Schema, "globalImageRegistryMirror": GlobalImageRegistryMirror{}.VariableSchema().OpenAPIV3Schema, + "users": Users{}.VariableSchema().OpenAPIV3Schema, }, }, } @@ -344,6 +348,83 @@ func (ImageRegistries) VariableSchema() clusterv1.VariableSchema { } } +type Users []User + +func (Users) VariableSchema() clusterv1.VariableSchema { + return clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Description: "Users to add to the machine", + Type: "array", + Items: ptr.To(User{}.VariableSchema().OpenAPIV3Schema), + }, + } +} + +// User defines the input for a generated user in cloud-init. +type User struct { + // Name specifies the user name. + Name string `json:"name"` + + // HashedPassword is a hashed password for the user, formatted as described + // by the crypt(5) man page. See your distribution's documentation for + // instructions to create a hashed password. + // An empty string is not marshalled, because it is not a valid value. + // +optional + HashedPassword string `json:"hashedPassword,omitempty"` + + // SSHAuthorizedKeys is a list of public SSH keys to write to the + // machine. Use the corresponding private SSH keys to authenticate. See SSH + // documentation for instructions to create a key pair. + // +optional + SSHAuthorizedKeys []string `json:"sshAuthorizedKeys,omitempty"` + + // Sudo is a sudo user specification, formatted as described in the sudo + // documentation. + // An empty string is not marshalled, because it is not a valid value. + // +optional + Sudo string `json:"sudo,omitempty"` +} + +func (User) VariableSchema() clusterv1.VariableSchema { + return clusterv1.VariableSchema{ + OpenAPIV3Schema: clusterv1.JSONSchemaProps{ + Type: "object", + Required: []string{"name"}, + Properties: map[string]clusterv1.JSONSchemaProps{ + "name": { + Description: "The username", + Type: "string", + }, + "hashedPassword": { + Description: "The hashed password for the user. Must be in the format of some hash function supported by the OS.", + Type: "string", + // The crypt (5) man page lists regexes for supported hash + // functions. We could validate input against a set of + // regexes, but because the set may be different from the + // set supported by the chosen OS, we might return a false + // negative or positive. For this reason, we do not validate + // the input. + }, + "sshAuthorizedKeys": { + Description: "A list of SSH authorized keys for this user", + Type: "array", + Items: &clusterv1.JSONSchemaProps{ + // No description, because the one for the parent array is enough. + Type: "string", + }, + }, + "sudo": { + Description: "The sudo rule that applies to this user", + Type: "string", + // A sudo rule is defined using an EBNF grammar, and must be + // parsed to be validated. We have decided to not integrate + // a sudo rule parser, so we do not validate the input. + }, + }, + }, + } +} + func init() { SchemeBuilder.Register(&ClusterConfig{}) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5af92e536..e555b38fe 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -472,6 +472,13 @@ func (in *GenericClusterConfig) DeepCopyInto(out *GenericClusterConfig) { *out = new(Addons) (*in).DeepCopyInto(*out) } + if in.Users != nil { + in, out := &in.Users, &out.Users + *out = make(Users, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericClusterConfig. @@ -764,6 +771,47 @@ func (in Subnets) DeepCopy() Subnets { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *User) DeepCopyInto(out *User) { + *out = *in + if in.SSHAuthorizedKeys != nil { + in, out := &in.SSHAuthorizedKeys, &out.SSHAuthorizedKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new User. +func (in *User) DeepCopy() *User { + if in == nil { + return nil + } + out := new(User) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Users) DeepCopyInto(out *Users) { + { + in := &in + *out = make(Users, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Users. +func (in Users) DeepCopy() Users { + if in == nil { + return nil + } + out := new(Users) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VPC) DeepCopyInto(out *VPC) { *out = *in diff --git a/docs/content/customization/generic/users.md b/docs/content/customization/generic/users.md new file mode 100644 index 000000000..b7f7c304c --- /dev/null +++ b/docs/content/customization/generic/users.md @@ -0,0 +1,63 @@ ++++ +title = "Users" ++++ + +Configure users for all machines in the cluster, the user's superuser capabilities using `sudo` user specifications, and +the login authentication mechanism. + +> - SSH _authorized keys_ are just public SSH keys that are used to authenticate a login. See the [SSH man +> page](https://www.man7.org/linux/man-pages/man8/sshd.8.html#AUTHORIZED_KEYS_FILE_FORMAT) for more information. +> +> - For information on sudo user specifications, see the [sudo +> documentation](https://www.sudo.ws/docs/man/sudoers.man/#User_specification). +> +> - Local password authentication is disabled for the user by default. It is enabled only when a hashed password is +> provided. + +## Examples + +### Admin user with SSH public key login + +Creates a user with the name `admin`, grants the user the ability to run any command as the superuser, and allows you to +login via SSH using the username and private key corresponding to the authorized public key. + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + variables: + - name: clusterConfig + value: + users: + - name: admin + - sshAuthorizedKeys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAua0lo8BiGWgvIiDCKnQDKL5uERHfnehm0ns5CEJpJw optionalcomment" + sudo: "ALL=(ALL) NOPASSWD:ALL" +``` + +### Admin user with serial console password login + +Creates a user with the name `admin,` grants the user the ability to run any command as the superuser, and allows you to +login via serial console using the username and password. + +> Note that this does not allow you to login via SSH using the username and password; in most cases, you must also +> configure the SSH server to allow password authentication. + +```yaml +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: +spec: + topology: + variables: + - name: clusterConfig + value: + users: + - name: admin + hashedPassword: "$y$j9T$UraH8eN4XvapXBmmSaUrP0$Nyxdf1cJDGZcp0WDKu.CFHprrkPG4ubirqSqiD43Ix3" + sudo: "ALL=(ALL) NOPASSWD:ALL" +``` diff --git a/go.mod b/go.mod index c1df13beb..c55396282 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api v0.0.0-00010101000000-000000000000 github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common v0.0.0-00010101000000-000000000000 github.com/go-logr/logr v1.4.1 + github.com/google/go-cmp v0.6.0 github.com/onsi/ginkgo/v2 v2.16.0 github.com/onsi/gomega v1.31.1 github.com/spf13/pflag v1.0.5 @@ -71,7 +72,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/cel-go v0.17.7 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-github/v53 v53.2.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/pkg/handlers/aws/mutation/metapatch_handler_test.go b/pkg/handlers/aws/mutation/metapatch_handler_test.go index e09beec54..f8c7f1c0f 100644 --- a/pkg/handlers/aws/mutation/metapatch_handler_test.go +++ b/pkg/handlers/aws/mutation/metapatch_handler_test.go @@ -37,6 +37,8 @@ import ( kubernetesimagerepositorytests "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/kubernetesimagerepository/tests" "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors" globalimageregistrymirrortests "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors/tests" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users" + userstests "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users/tests" "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/workerconfig" ) @@ -188,4 +190,11 @@ func TestGeneratePatches(t *testing.T) { v1alpha1.AWSVariableName, controlplaneloadbalancer.VariableName, ) + + userstests.TestGeneratePatches( + t, + metaPatchGeneratorFunc(mgr), + clusterconfig.MetaVariableName, + users.VariableName, + ) } diff --git a/pkg/handlers/docker/mutation/metapatch_handler_test.go b/pkg/handlers/docker/mutation/metapatch_handler_test.go index 0004da3b2..cdcd81994 100644 --- a/pkg/handlers/docker/mutation/metapatch_handler_test.go +++ b/pkg/handlers/docker/mutation/metapatch_handler_test.go @@ -27,6 +27,8 @@ import ( kubernetesimagerepositorytests "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/kubernetesimagerepository/tests" "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors" globalimageregistrymirrortests "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors/tests" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users" + userstests "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users/tests" "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/workerconfig" ) @@ -112,4 +114,11 @@ func TestGeneratePatches(t *testing.T) { clusterconfig.MetaVariableName, mirrors.GlobalMirrorVariableName, ) + + userstests.TestGeneratePatches( + t, + metaPatchGeneratorFunc(mgr), + clusterconfig.MetaVariableName, + users.VariableName, + ) } diff --git a/pkg/handlers/generic/mutation/handlers.go b/pkg/handlers/generic/mutation/handlers.go index 087582b0f..bd71c8caa 100644 --- a/pkg/handlers/generic/mutation/handlers.go +++ b/pkg/handlers/generic/mutation/handlers.go @@ -15,6 +15,7 @@ import ( "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/imageregistries/credentials" "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/kubernetesimagerepository" "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/mirrors" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/mutation/users" ) // MetaMutators returns all generic patch handlers. @@ -28,5 +29,6 @@ func MetaMutators(mgr manager.Manager) []mutation.MetaMutator { credentials.NewPatch(mgr.GetClient()), mirrors.NewPatch(mgr.GetClient()), calico.NewPatch(), + users.NewPatch(), } } diff --git a/pkg/handlers/generic/mutation/users/doc.go b/pkg/handlers/generic/mutation/users/doc.go new file mode 100644 index 000000000..4584d2d23 --- /dev/null +++ b/pkg/handlers/generic/mutation/users/doc.go @@ -0,0 +1,4 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package users diff --git a/pkg/handlers/generic/mutation/users/inject.go b/pkg/handlers/generic/mutation/users/inject.go new file mode 100644 index 000000000..9d41e0495 --- /dev/null +++ b/pkg/handlers/generic/mutation/users/inject.go @@ -0,0 +1,147 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package users + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/utils/ptr" + bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" + controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/patches/selectors" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/variables" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/clusterconfig" +) + +const ( + // VariableName is the external patch variable name. + VariableName = "users" +) + +type usersPatchHandler struct { + variableName string + variableFieldPath []string +} + +func NewPatch() *usersPatchHandler { + return newUsersPatchHandler( + clusterconfig.MetaVariableName, + VariableName) +} + +func newUsersPatchHandler( + variableName string, + variableFieldPath ...string, +) *usersPatchHandler { + return &usersPatchHandler{ + variableName: variableName, + variableFieldPath: variableFieldPath, + } +} + +func (h *usersPatchHandler) Mutate( + ctx context.Context, + obj *unstructured.Unstructured, + vars map[string]apiextensionsv1.JSON, + holderRef runtimehooksv1.HolderReference, + _ ctrlclient.ObjectKey, +) error { + log := ctrl.LoggerFrom(ctx, "holderRef", holderRef) + + usersVariable, found, err := variables.Get[v1alpha1.Users]( + vars, + h.variableName, + h.variableFieldPath..., + ) + if err != nil { + return err + } + if !found { + log.V(5).Info("users variable not defined") + return nil + } + + log = log.WithValues( + "variableName", + h.variableName, + "variableFieldPath", + h.variableFieldPath, + "variableValue", + usersVariable, + ) + + if err := patches.MutateIfApplicable( + obj, vars, &holderRef, selectors.ControlPlane(), log, + func(obj *controlplanev1.KubeadmControlPlaneTemplate) error { + log.WithValues( + "patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(), + "patchedObjectName", ctrlclient.ObjectKeyFromObject(obj), + ).Info("setting users in control plane kubeadm config template") + bootstrapUsers := []bootstrapv1.User{} + for _, userFromVariable := range usersVariable { + bootstrapUsers = append(bootstrapUsers, generateBootstrapUser(userFromVariable)) + } + obj.Spec.Template.Spec.KubeadmConfigSpec.Users = bootstrapUsers + return nil + }); err != nil { + return err + } + + if err := patches.MutateIfApplicable( + obj, vars, &holderRef, selectors.WorkersKubeadmConfigTemplateSelector(), log, + func(obj *bootstrapv1.KubeadmConfigTemplate) error { + log.WithValues( + "patchedObjectKind", obj.GetObjectKind().GroupVersionKind().String(), + "patchedObjectName", ctrlclient.ObjectKeyFromObject(obj), + ).Info("setting users in worker node kubeadm config template") + bootstrapUsers := []bootstrapv1.User{} + for _, userFromVariable := range usersVariable { + bootstrapUsers = append(bootstrapUsers, generateBootstrapUser(userFromVariable)) + } + obj.Spec.Template.Spec.Users = bootstrapUsers + return nil + }); err != nil { + return err + } + + return nil +} + +func generateBootstrapUser(userFromVariable v1alpha1.User) bootstrapv1.User { + bootstrapUser := bootstrapv1.User{ + Name: userFromVariable.Name, + SSHAuthorizedKeys: userFromVariable.SSHAuthorizedKeys, + } + + // LockPassword is not part of our API, because we can derive its value + // for the use cases our API supports. + // + // We do not support these edge cases: + // (a) Hashed password is defined, password authentication is not enabled. + // (b) Hashed password is not defined, password authentication is enabled. + // + // We disable password authentication by default. + bootstrapUser.LockPassword = ptr.To(true) + + if userFromVariable.HashedPassword != "" { + // We enable password authentication only if a hashed password is defined. + bootstrapUser.LockPassword = ptr.To(false) + + bootstrapUser.Passwd = ptr.To(userFromVariable.HashedPassword) + } + + if userFromVariable.Sudo != "" { + bootstrapUser.Sudo = ptr.To(userFromVariable.Sudo) + } + + return bootstrapUser +} diff --git a/pkg/handlers/generic/mutation/users/inject_test.go b/pkg/handlers/generic/mutation/users/inject_test.go new file mode 100644 index 000000000..22eb1c812 --- /dev/null +++ b/pkg/handlers/generic/mutation/users/inject_test.go @@ -0,0 +1,116 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package users + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "k8s.io/utils/ptr" + bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" + + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1" +) + +func Test_generateBootstrapUser(t *testing.T) { + type args struct { + userFromVariable v1alpha1.User + } + tests := []struct { + name string + args args + want bootstrapv1.User + }{ + { + name: "if user sets hashed password, enable password auth and set passwd", + args: args{ + userFromVariable: v1alpha1.User{ + Name: "example", + HashedPassword: "example", + }, + }, + want: bootstrapv1.User{ + Name: "example", + Passwd: ptr.To("example"), + LockPassword: ptr.To(false), + }, + }, + { + name: "if user does not set hashed password, disable password auth and do not set passwd", + args: args{ + userFromVariable: v1alpha1.User{ + Name: "example", + }, + }, + want: bootstrapv1.User{ + Name: "example", + Passwd: nil, + LockPassword: ptr.To(true), + }, + }, + { + name: "if user sets empty hashed password, disable password auth and do not set passwd", + args: args{ + userFromVariable: v1alpha1.User{ + Name: "example", + HashedPassword: "", + }, + }, + want: bootstrapv1.User{ + Name: "example", + Passwd: nil, + LockPassword: ptr.To(true), + }, + }, + { + name: "if user sets sudo, include it in the patch", + args: args{ + userFromVariable: v1alpha1.User{ + Name: "example", + Sudo: "example", + }, + }, + want: bootstrapv1.User{ + Name: "example", + Sudo: ptr.To("example"), + LockPassword: ptr.To(true), + }, + }, + { + name: "if user does not set sudo, do not include in the patch", + args: args{ + userFromVariable: v1alpha1.User{ + Name: "example", + }, + }, + want: bootstrapv1.User{ + Name: "example", + Sudo: nil, + LockPassword: ptr.To(true), + }, + }, + { + name: "if user sets empty sudo, do not include in the patch", + args: args{ + userFromVariable: v1alpha1.User{ + Name: "example", + Sudo: "", + }, + }, + want: bootstrapv1.User{ + Name: "example", + Sudo: nil, + LockPassword: ptr.To(true), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := generateBootstrapUser(tt.args.userFromVariable) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("generateBootstrapUser() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/handlers/generic/mutation/users/tests/generate_patches.go b/pkg/handlers/generic/mutation/users/tests/generate_patches.go new file mode 100644 index 000000000..2eff072f5 --- /dev/null +++ b/pkg/handlers/generic/mutation/users/tests/generate_patches.go @@ -0,0 +1,97 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package tests + +import ( + "testing" + + "github.com/onsi/gomega" + "k8s.io/apiserver/pkg/storage/names" + runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" + + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/capi/clustertopology/handlers/mutation" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest/request" +) + +var ( + testUser1 = v1alpha1.User{ + Name: "complete", + HashedPassword: "password", + SSHAuthorizedKeys: []string{ + "key1", + "key2", + }, + Sudo: "ALL=(ALL) NOPASSWD:ALL", + } + testUser2 = v1alpha1.User{ + Name: "onlyname", + } +) + +func TestGeneratePatches( + t *testing.T, + generatorFunc func() mutation.GeneratePatches, + variableName string, + variablePath ...string, +) { + t.Helper() + + capitest.ValidateGeneratePatches( + t, + generatorFunc, + capitest.PatchTestDef{ + Name: "unset variable", + }, + capitest.PatchTestDef{ + Name: "users set for KubeadmControlPlaneTemplate", + Vars: []runtimehooksv1.Variable{ + capitest.VariableWithValue( + variableName, + []v1alpha1.User{testUser1, testUser2}, + variablePath..., + ), + }, + RequestItem: request.NewKubeadmControlPlaneTemplateRequestItem(""), + ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{ + Operation: "add", + Path: "/spec/template/spec/kubeadmConfigSpec/users", + ValueMatcher: gomega.HaveLen(2), + }}, + }, + ) + + capitest.ValidateGeneratePatches( + t, + generatorFunc, + capitest.PatchTestDef{ + Name: "unset variable", + }, + capitest.PatchTestDef{ + Name: "users set for KubeadmConfigTemplate generic worker", + Vars: []runtimehooksv1.Variable{ + capitest.VariableWithValue( + variableName, + []v1alpha1.User{testUser1, testUser2}, + variablePath..., + ), + capitest.VariableWithValue( + "builtin", + map[string]any{ + "machineDeployment": map[string]any{ + "class": names.SimpleNameGenerator.GenerateName("worker-"), + }, + }, + ), + }, + RequestItem: request.NewKubeadmConfigTemplateRequestItem(""), + ExpectedPatchMatchers: []capitest.JSONPatchMatcher{{ + Operation: "add", + Path: "/spec/template/spec/users", + ValueMatcher: gomega.HaveLen(2), + }}, + }, + ) +} diff --git a/pkg/handlers/generic/mutation/users/variables_test.go b/pkg/handlers/generic/mutation/users/variables_test.go new file mode 100644 index 000000000..227318484 --- /dev/null +++ b/pkg/handlers/generic/mutation/users/variables_test.go @@ -0,0 +1,43 @@ +// Copyright 2023 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package users + +import ( + "testing" + + "k8s.io/utils/ptr" + + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/api/v1alpha1" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/common/pkg/testutils/capitest" + "github.com/d2iq-labs/cluster-api-runtime-extensions-nutanix/pkg/handlers/generic/clusterconfig" +) + +func TestVariableValidation(t *testing.T) { + capitest.ValidateDiscoverVariables( + t, + clusterconfig.MetaVariableName, + ptr.To(v1alpha1.GenericClusterConfig{}.VariableSchema()), + false, + clusterconfig.NewVariable, + capitest.VariableTestDef{ + Name: "valid users", + Vals: v1alpha1.GenericClusterConfig{ + Users: []v1alpha1.User{ + { + Name: "complete", + HashedPassword: "password", + SSHAuthorizedKeys: []string{ + "key1", + "key2", + }, + Sudo: "ALL=(ALL) NOPASSWD:ALL", + }, + { + Name: "onlyname", + }, + }, + }, + }, + ) +}