From 51f3f0717c14f31bc737661a0fd80e028f93a29c Mon Sep 17 00:00:00 2001 From: Dinar Valeev Date: Mon, 27 Oct 2025 23:46:23 +0100 Subject: [PATCH] Add AWSManagedPoolTemplate Signed-off-by: Dinar Valeev --- ...k8s.io_awsmanagedmachinepooltemplates.yaml | 654 ++++++++++++++++++ .../v1beta2/awsmanagedmachinepool_webhook.go | 110 +-- .../awsmanagedmachinepooltemplate_types.go | 56 ++ .../awsmanagedmachinepooltemplate_webhook.go | 146 ++++ exp/api/v1beta2/zz_generated.deepcopy.go | 94 +++ main.go | 4 + 6 files changed, 1011 insertions(+), 53 deletions(-) create mode 100644 config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepooltemplates.yaml create mode 100644 exp/api/v1beta2/awsmanagedmachinepooltemplate_types.go create mode 100644 exp/api/v1beta2/awsmanagedmachinepooltemplate_webhook.go diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepooltemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepooltemplates.yaml new file mode 100644 index 0000000000..228a713cb0 --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepooltemplates.yaml @@ -0,0 +1,654 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + name: awsmanagedmachinepooltemplates.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: AWSManagedMachinePoolTemplate + listKind: AWSManagedMachinePoolTemplateList + plural: awsmanagedmachinepooltemplates + shortNames: + - awsmmpt + singular: awsmanagedmachinepooltemplate + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: AWSManagedMachinePoolTemplate is the Schema for the awsmanagedmachinepooltemplates + API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: AWSManagedMachinePoolTemplateSpec defines the desired state + of AWSManagedMachinePoolTemplate. + properties: + template: + description: AWSManagedMachinePoolTemplateResource wraps AWSManagedMachinePoolSpec + properties: + spec: + description: AWSManagedMachinePoolSpec defines the desired state + of AWSManagedMachinePool. + properties: + additionalTags: + additionalProperties: + type: string + description: |- + AdditionalTags is an optional set of tags to add to AWS resources managed by the AWS provider, in addition to the + ones added by default. + type: object + amiType: + default: AL2_x86_64 + description: AMIType defines the AMI type + enum: + - AL2_x86_64 + - AL2_x86_64_GPU + - AL2_ARM_64 + - CUSTOM + - BOTTLEROCKET_ARM_64 + - BOTTLEROCKET_x86_64 + - BOTTLEROCKET_ARM_64_FIPS + - BOTTLEROCKET_x86_64_FIPS + - BOTTLEROCKET_ARM_64_NVIDIA + - BOTTLEROCKET_x86_64_NVIDIA + - WINDOWS_CORE_2019_x86_64 + - WINDOWS_FULL_2019_x86_64 + - WINDOWS_CORE_2022_x86_64 + - WINDOWS_FULL_2022_x86_64 + - AL2023_x86_64_STANDARD + - AL2023_ARM_64_STANDARD + - AL2023_x86_64_NEURON + - AL2023_x86_64_NVIDIA + - AL2023_ARM_64_NVIDIA + type: string + amiVersion: + description: |- + AMIVersion defines the desired AMI release version. If no version number + is supplied then the latest version for the Kubernetes version + will be used + minLength: 2 + type: string + availabilityZoneSubnetType: + description: AvailabilityZoneSubnetType specifies which type + of subnets to use when an availability zone is specified. + enum: + - public + - private + - all + type: string + availabilityZones: + description: AvailabilityZones is an array of availability + zones instances can run in + items: + type: string + type: array + awsLaunchTemplate: + description: |- + AWSLaunchTemplate specifies the launch template to use to create the managed node group. + If AWSLaunchTemplate is specified, certain node group configuraions outside of launch template + are prohibited (https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html). + properties: + additionalSecurityGroups: + description: |- + AdditionalSecurityGroups is an array of references to security groups that should be applied to the + instances. These security groups would be set in addition to any security groups defined + at the cluster level or in the actuator. + items: + description: |- + AWSResourceReference is a reference to a specific AWS resource by ID or filters. + Only one of ID or Filters may be specified. Specifying more than one will result in + a validation error. + properties: + filters: + description: |- + Filters is a set of key/value pairs used to identify a resource + They are applied according to the rules defined by the AWS API: + https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Filtering.html + items: + description: Filter is a filter used to identify + an AWS resource. + properties: + name: + description: Name of the filter. Filter names + are case-sensitive. + type: string + values: + description: Values includes one or more filter + values. Filter values are case-sensitive. + items: + type: string + type: array + required: + - name + - values + type: object + type: array + id: + description: ID of resource + type: string + type: object + type: array + ami: + description: AMI is the reference to the AMI from which + to create the machine instance. + properties: + eksLookupType: + description: EKSOptimizedLookupType If specified, + will look up an EKS Optimized image in SSM Parameter + store + enum: + - AmazonLinux + - AmazonLinuxGPU + - AmazonLinux2023 + - AmazonLinux2023GPU + type: string + id: + description: ID of resource + type: string + type: object + capacityReservationId: + description: CapacityReservationID specifies the target + Capacity Reservation into which the instance should + be launched. + type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string + iamInstanceProfile: + description: |- + The name or the Amazon Resource Name (ARN) of the instance profile associated + with the IAM role for the instance. The instance profile contains the IAM + role. + type: string + imageLookupBaseOS: + description: |- + ImageLookupBaseOS is the name of the base operating system to use for + image lookup the AMI is not set. + type: string + imageLookupFormat: + description: |- + ImageLookupFormat is the AMI naming format to look up the image for this + machine It will be ignored if an explicit AMI is set. Supports + substitutions for {{.BaseOS}} and {{.K8sVersion}} with the base OS and + kubernetes version, respectively. The BaseOS will be the value in + ImageLookupBaseOS or ubuntu (the default), and the kubernetes version as + defined by the packages produced by kubernetes/release without v as a + prefix: 1.13.0, 1.12.5-mybuild.1, or 1.17.3. For example, the default + image format of capa-ami-{{.BaseOS}}-?{{.K8sVersion}}-* will end up + searching for AMIs that match the pattern capa-ami-ubuntu-?1.18.0-* for a + Machine that is targeting kubernetes v1.18.0 and the ubuntu base OS. See + also: https://golang.org/pkg/text/template/ + type: string + imageLookupOrg: + description: ImageLookupOrg is the AWS Organization ID + to use for image lookup if AMI is not set. + type: string + instanceMetadataOptions: + description: InstanceMetadataOptions defines the behavior + for applying metadata to instances. + properties: + httpEndpoint: + default: enabled + description: |- + Enables or disables the HTTP metadata endpoint on your instances. + + If you specify a value of disabled, you cannot access your instance metadata. + + Default: enabled + enum: + - enabled + - disabled + type: string + httpPutResponseHopLimit: + default: 1 + description: |- + The desired HTTP PUT response hop limit for instance metadata requests. The + larger the number, the further instance metadata requests can travel. + + Default: 1 + format: int64 + maximum: 64 + minimum: 1 + type: integer + httpTokens: + default: optional + description: |- + The state of token usage for your instance metadata requests. + + If the state is optional, you can choose to retrieve instance metadata with + or without a session token on your request. If you retrieve the IAM role + credentials without a token, the version 1.0 role credentials are returned. + If you retrieve the IAM role credentials using a valid session token, the + version 2.0 role credentials are returned. + + If the state is required, you must send a session token with any instance + metadata retrieval requests. In this state, retrieving the IAM role credentials + always returns the version 2.0 credentials; the version 1.0 credentials are + not available. + + Default: optional + enum: + - optional + - required + type: string + instanceMetadataTags: + default: disabled + description: |- + Set to enabled to allow access to instance tags from the instance metadata. + Set to disabled to turn off access to instance tags from the instance metadata. + For more information, see Work with instance tags using the instance metadata + (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html#work-with-tags-in-IMDS). + + Default: disabled + enum: + - enabled + - disabled + type: string + type: object + instanceType: + description: 'InstanceType is the type of instance to + create. Example: m4.xlarge' + type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "OnDemand" (default): The instance runs as a standard OnDemand instance. + "Spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the marketType defaults to "Spot". + "CapacityBlock": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If marketType is not specified and spotMarketOptions is provided, the marketType defaults to "Spot". + enum: + - OnDemand + - Spot + - CapacityBlock + type: string + name: + description: The name of the launch template. + type: string + nonRootVolumes: + description: Configuration options for the non root storage + volumes. + items: + description: Volume encapsulates the configuration options + for the storage device. + properties: + deviceName: + description: Device name + type: string + encrypted: + description: Encrypted is whether the volume should + be encrypted or not. + type: boolean + encryptionKey: + description: |- + EncryptionKey is the KMS key to use to encrypt the volume. Can be either a KMS key ID or ARN. + If Encrypted is set and this is omitted, the default AWS key will be used. + The key must already exist and be accessible by the controller. + type: string + iops: + description: IOPS is the number of IOPS requested + for the disk. Not applicable to all types. + format: int64 + type: integer + size: + description: |- + Size specifies size (in Gi) of the storage device. + Must be greater than the image snapshot size or 8 (whichever is greater). + format: int64 + minimum: 8 + type: integer + throughput: + description: Throughput to provision in MiB/s supported + for the volume type. Not applicable to all types. + format: int64 + type: integer + type: + description: Type is the type of the volume (e.g. + gp2, io1, etc...). + type: string + required: + - size + type: object + type: array + privateDnsName: + description: PrivateDNSName is the options for the instance + hostname. + properties: + enableResourceNameDnsAAAARecord: + description: EnableResourceNameDNSAAAARecord indicates + whether to respond to DNS queries for instance hostnames + with DNS AAAA records. + type: boolean + enableResourceNameDnsARecord: + description: EnableResourceNameDNSARecord indicates + whether to respond to DNS queries for instance hostnames + with DNS A records. + type: boolean + hostnameType: + description: The type of hostname to assign to an + instance. + enum: + - ip-name + - resource-name + type: string + type: object + rootVolume: + description: RootVolume encapsulates the configuration + options for the root volume + properties: + deviceName: + description: Device name + type: string + encrypted: + description: Encrypted is whether the volume should + be encrypted or not. + type: boolean + encryptionKey: + description: |- + EncryptionKey is the KMS key to use to encrypt the volume. Can be either a KMS key ID or ARN. + If Encrypted is set and this is omitted, the default AWS key will be used. + The key must already exist and be accessible by the controller. + type: string + iops: + description: IOPS is the number of IOPS requested + for the disk. Not applicable to all types. + format: int64 + type: integer + size: + description: |- + Size specifies size (in Gi) of the storage device. + Must be greater than the image snapshot size or 8 (whichever is greater). + format: int64 + minimum: 8 + type: integer + throughput: + description: Throughput to provision in MiB/s supported + for the volume type. Not applicable to all types. + format: int64 + type: integer + type: + description: Type is the type of the volume (e.g. + gp2, io1, etc...). + type: string + required: + - size + type: object + spotMarketOptions: + description: SpotMarketOptions are options for configuring + AWSMachinePool instances to be run using AWS Spot instances. + properties: + maxPrice: + description: MaxPrice defines the maximum price the + user is willing to pay for Spot VM instances + type: string + type: object + sshKeyName: + description: |- + SSHKeyName is the name of the ssh key to attach to the instance. Valid values are empty string + (do not use SSH keys), a valid SSH key name, or omitted (use the default SSH key name) + type: string + versionNumber: + description: |- + VersionNumber is the version of the launch template that is applied. + Typically a new version is created when at least one of the following happens: + 1) A new launch template spec is applied. + 2) One or more parameters in an existing template is changed. + 3) A new AMI is discovered. + format: int64 + type: integer + type: object + capacityType: + default: onDemand + description: CapacityType specifies the capacity type for + the ASG behind this pool + enum: + - onDemand + - spot + type: string + diskSize: + description: DiskSize specifies the root disk size + format: int32 + type: integer + eksNodegroupName: + description: |- + EKSNodegroupName specifies the name of the nodegroup in AWS + corresponding to this MachinePool. If you don't specify a name + then a default name will be created based on the namespace and + name of the managed machine pool. + type: string + instanceType: + description: InstanceType specifies the AWS instance type + type: string + labels: + additionalProperties: + type: string + description: Labels specifies labels for the Kubernetes node + objects + type: object + lifecycleHooks: + description: AWSLifecycleHooks specifies lifecycle hooks for + the managed node group. + items: + description: AWSLifecycleHook describes an AWS lifecycle + hook + properties: + defaultResult: + description: The default result for the lifecycle hook. + The possible values are CONTINUE and ABANDON. + enum: + - CONTINUE + - ABANDON + type: string + heartbeatTimeout: + description: |- + The maximum time, in seconds, that an instance can remain in a Pending:Wait or + Terminating:Wait state. The maximum is 172800 seconds (48 hours) or 100 times + HeartbeatTimeout, whichever is smaller. + format: duration + type: string + lifecycleTransition: + description: The state of the EC2 instance to which + to attach the lifecycle hook. + enum: + - autoscaling:EC2_INSTANCE_LAUNCHING + - autoscaling:EC2_INSTANCE_TERMINATING + type: string + name: + description: The name of the lifecycle hook. + type: string + notificationMetadata: + description: Contains additional metadata that will + be passed to the notification target. + type: string + notificationTargetARN: + description: |- + The ARN of the notification target that Amazon EC2 Auto Scaling uses to + notify you when an instance is in the transition state for the lifecycle hook. + type: string + roleARN: + description: |- + The ARN of the IAM role that allows the Auto Scaling group to publish to the + specified notification target. + type: string + required: + - lifecycleTransition + - name + type: object + type: array + providerIDList: + description: |- + ProviderIDList are the provider IDs of instances in the + autoscaling group corresponding to the nodegroup represented by this + machine pool + items: + type: string + type: array + remoteAccess: + description: RemoteAccess specifies how machines can be accessed + remotely + properties: + public: + description: Public specifies whether to open port 22 + to the public internet + type: boolean + sourceSecurityGroups: + description: SourceSecurityGroups specifies which security + groups are allowed access + items: + type: string + type: array + sshKeyName: + description: |- + SSHKeyName specifies which EC2 SSH key can be used to access machines. + If left empty, the key from the control plane is used. + type: string + type: object + roleAdditionalPolicies: + description: |- + RoleAdditionalPolicies allows you to attach additional polices to + the node group role. You must enable the EKSAllowAddRoles + feature flag to incorporate these into the created role. + items: + type: string + type: array + roleName: + description: |- + RoleName specifies the name of IAM role for the node group. + If the role is pre-existing we will treat it as unmanaged + and not delete it on deletion. If the EKSEnableIAM feature + flag is true and no name is supplied then a role is created. + type: string + rolePath: + description: |- + RolePath sets the path to the role. For more information about paths, see IAM Identifiers + (https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) + in the IAM User Guide. + + This parameter is optional. If it is not included, it defaults to a slash + (/). + type: string + rolePermissionsBoundary: + description: |- + RolePermissionsBoundary sets the ARN of the managed policy that is used + to set the permissions boundary for the role. + + A permissions boundary policy defines the maximum permissions that identity-based + policies can grant to an entity, but does not grant permissions. Permissions + boundaries do not define the maximum permissions that a resource-based policy + can grant to an entity. To learn more, see Permissions boundaries for IAM + entities (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) + in the IAM User Guide. + + For more information about policy types, see Policy types (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policy-types) + in the IAM User Guide. + type: string + scaling: + description: Scaling specifies scaling for the ASG behind + this pool + properties: + maxSize: + format: int32 + type: integer + minSize: + format: int32 + type: integer + type: object + subnetIDs: + description: |- + SubnetIDs specifies which subnets are used for the + auto scaling group of this nodegroup + items: + type: string + type: array + taints: + description: Taints specifies the taints to apply to the nodes + of the machine pool + items: + description: Taint defines the specs for a Kubernetes taint. + properties: + effect: + description: Effect specifies the effect for the taint + enum: + - no-schedule + - no-execute + - prefer-no-schedule + type: string + key: + description: Key is the key of the taint + type: string + value: + description: Value is the value of the taint + type: string + required: + - effect + - key + - value + type: object + type: array + updateConfig: + description: |- + UpdateConfig holds the optional config to control the behaviour of the update + to the nodegroup. + properties: + maxUnavailable: + description: |- + MaxUnavailable is the maximum number of nodes unavailable at once during a version update. + Nodes will be updated in parallel. The maximum number is 100. + maximum: 100 + minimum: 1 + type: integer + maxUnavailablePercentage: + description: |- + MaxUnavailablePercentage is the maximum percentage of nodes unavailable during a version update. This + percentage of nodes will be updated in parallel, up to 100 nodes at once. + maximum: 100 + minimum: 1 + type: integer + type: object + type: object + required: + - spec + type: object + required: + - template + type: object + type: object + served: true + storage: true diff --git a/exp/api/v1beta2/awsmanagedmachinepool_webhook.go b/exp/api/v1beta2/awsmanagedmachinepool_webhook.go index 38ceffe3f3..0469c1ad52 100644 --- a/exp/api/v1beta2/awsmanagedmachinepool_webhook.go +++ b/exp/api/v1beta2/awsmanagedmachinepool_webhook.go @@ -60,13 +60,13 @@ type awsManagedMachinePoolWebhook struct{} var _ webhook.CustomDefaulter = &awsManagedMachinePoolWebhook{} var _ webhook.CustomValidator = &awsManagedMachinePoolWebhook{} -func (r *AWSManagedMachinePool) validateScaling() field.ErrorList { +func validateScaling(r *AWSManagedMachinePoolSpec) field.ErrorList { var allErrs field.ErrorList - if r.Spec.Scaling != nil { //nolint:nestif + if r.Scaling != nil { //nolint:nestif minField := field.NewPath("spec", "scaling", "minSize") maxField := field.NewPath("spec", "scaling", "maxSize") - minSize := r.Spec.Scaling.MinSize - maxSize := r.Spec.Scaling.MaxSize + minSize := r.Scaling.MinSize + maxSize := r.Scaling.MaxSize if minSize != nil { if *minSize < 0 { allErrs = append(allErrs, field.Invalid(minField, *minSize, "must be greater or equal zero")) @@ -85,18 +85,18 @@ func (r *AWSManagedMachinePool) validateScaling() field.ErrorList { return allErrs } -func (r *AWSManagedMachinePool) validateNodegroupUpdateConfig() field.ErrorList { +func validateNodegroupUpdateConfig(r *AWSManagedMachinePoolSpec) field.ErrorList { var allErrs field.ErrorList - if r.Spec.UpdateConfig != nil { + if r.UpdateConfig != nil { nodegroupUpdateConfigField := field.NewPath("spec", "updateConfig") - if r.Spec.UpdateConfig.MaxUnavailable == nil && r.Spec.UpdateConfig.MaxUnavailablePercentage == nil { - allErrs = append(allErrs, field.Invalid(nodegroupUpdateConfigField, r.Spec.UpdateConfig, "must specify one of maxUnavailable or maxUnavailablePercentage when using nodegroup updateconfig")) + if r.UpdateConfig.MaxUnavailable == nil && r.UpdateConfig.MaxUnavailablePercentage == nil { + allErrs = append(allErrs, field.Invalid(nodegroupUpdateConfigField, r.UpdateConfig, "must specify one of maxUnavailable or maxUnavailablePercentage when using nodegroup updateconfig")) } - if r.Spec.UpdateConfig.MaxUnavailable != nil && r.Spec.UpdateConfig.MaxUnavailablePercentage != nil { - allErrs = append(allErrs, field.Invalid(nodegroupUpdateConfigField, r.Spec.UpdateConfig, "cannot specify both maxUnavailable and maxUnavailablePercentage")) + if r.UpdateConfig.MaxUnavailable != nil && r.UpdateConfig.MaxUnavailablePercentage != nil { + allErrs = append(allErrs, field.Invalid(nodegroupUpdateConfigField, r.UpdateConfig, "cannot specify both maxUnavailable and maxUnavailablePercentage")) } } @@ -106,15 +106,15 @@ func (r *AWSManagedMachinePool) validateNodegroupUpdateConfig() field.ErrorList return allErrs } -func (r *AWSManagedMachinePool) validateRemoteAccess() field.ErrorList { +func validateRemoteAccess(r *AWSManagedMachinePoolSpec) field.ErrorList { var allErrs field.ErrorList - if r.Spec.RemoteAccess == nil { + if r.RemoteAccess == nil { return allErrs } remoteAccessPath := field.NewPath("spec", "remoteAccess") - sourceSecurityGroups := r.Spec.RemoteAccess.SourceSecurityGroups + sourceSecurityGroups := r.RemoteAccess.SourceSecurityGroups - if public := r.Spec.RemoteAccess.Public; public && len(sourceSecurityGroups) > 0 { + if public := r.RemoteAccess.Public; public && len(sourceSecurityGroups) > 0 { allErrs = append( allErrs, field.Invalid(remoteAccessPath.Child("sourceSecurityGroups"), sourceSecurityGroups, "must be empty if public is set"), @@ -124,28 +124,28 @@ func (r *AWSManagedMachinePool) validateRemoteAccess() field.ErrorList { return allErrs } -func (r *AWSManagedMachinePool) validateLaunchTemplate() field.ErrorList { +func validateLaunchTemplate(r *AWSManagedMachinePoolSpec) field.ErrorList { var allErrs field.ErrorList - if r.Spec.AWSLaunchTemplate == nil { + if r.AWSLaunchTemplate == nil { return allErrs } - if r.Spec.InstanceType != nil { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "InstanceType"), r.Spec.InstanceType, "InstanceType cannot be specified when LaunchTemplate is specified")) + if r.InstanceType != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "InstanceType"), r.InstanceType, "InstanceType cannot be specified when LaunchTemplate is specified")) } - if r.Spec.DiskSize != nil { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "DiskSize"), r.Spec.DiskSize, "DiskSize cannot be specified when LaunchTemplate is specified")) + if r.DiskSize != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "DiskSize"), r.DiskSize, "DiskSize cannot be specified when LaunchTemplate is specified")) } - if r.Spec.AWSLaunchTemplate.IamInstanceProfile != "" { - allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "AWSLaunchTemplate", "IamInstanceProfile"), r.Spec.AWSLaunchTemplate.IamInstanceProfile, "IAM instance profile in launch template is prohibited in EKS managed node group")) + if r.AWSLaunchTemplate.IamInstanceProfile != "" { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "AWSLaunchTemplate", "IamInstanceProfile"), r.AWSLaunchTemplate.IamInstanceProfile, "IAM instance profile in launch template is prohibited in EKS managed node group")) } return allErrs } -func (r *AWSManagedMachinePool) validateLifecycleHooks() field.ErrorList { - return validateLifecycleHooks(r.Spec.AWSLifecycleHooks) +func (r *AWSManagedMachinePoolSpec) validateLifecycleHooks() field.ErrorList { + return validateLifecycleHooks(r.AWSLifecycleHooks) } // ValidateCreate will do any extra validation when creating a AWSManagedMachinePool. @@ -162,19 +162,19 @@ func (*awsManagedMachinePoolWebhook) ValidateCreate(_ context.Context, obj runti if r.Spec.EKSNodegroupName == "" { allErrs = append(allErrs, field.Required(field.NewPath("spec.eksNodegroupName"), "eksNodegroupName is required")) } - if errs := r.validateScaling(); errs != nil || len(errs) == 0 { + if errs := validateScaling(&r.Spec); errs != nil || len(errs) == 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateRemoteAccess(); len(errs) > 0 { + if errs := validateRemoteAccess(&r.Spec); len(errs) > 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 { + if errs := validateNodegroupUpdateConfig(&r.Spec); len(errs) > 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateLaunchTemplate(); len(errs) > 0 { + if errs := validateLaunchTemplate(&r.Spec); len(errs) > 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateLifecycleHooks(); len(errs) > 0 { + if errs := r.Spec.validateLifecycleHooks(); len(errs) > 0 { allErrs = append(allErrs, errs...) } @@ -207,19 +207,19 @@ func (*awsManagedMachinePoolWebhook) ValidateUpdate(_ context.Context, oldObj, n } var allErrs field.ErrorList - allErrs = append(allErrs, r.validateImmutable(oldPool)...) + allErrs = append(allErrs, validateAMPImmutable(&oldPool.Spec, &r.Spec)...) allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) - if errs := r.validateScaling(); errs != nil || len(errs) == 0 { + if errs := validateScaling(&r.Spec); errs != nil || len(errs) == 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 { + if errs := validateNodegroupUpdateConfig(&r.Spec); len(errs) > 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateLaunchTemplate(); len(errs) > 0 { + if errs := validateLaunchTemplate(&r.Spec); len(errs) > 0 { allErrs = append(allErrs, errs...) } - if errs := r.validateLifecycleHooks(); len(errs) > 0 { + if errs := r.Spec.validateLifecycleHooks(); len(errs) > 0 { allErrs = append(allErrs, errs...) } @@ -239,7 +239,7 @@ func (*awsManagedMachinePoolWebhook) ValidateDelete(_ context.Context, _ runtime return nil, nil } -func (r *AWSManagedMachinePool) validateImmutable(old *AWSManagedMachinePool) field.ErrorList { +func validateAMPImmutable(old *AWSManagedMachinePoolSpec, current *AWSManagedMachinePoolSpec) field.ErrorList { var allErrs field.ErrorList appendErrorIfMutated := func(old, update interface{}, name string) { @@ -259,26 +259,26 @@ func (r *AWSManagedMachinePool) validateImmutable(old *AWSManagedMachinePool) fi } } - if old.Spec.EKSNodegroupName != "" { - appendErrorIfMutated(old.Spec.EKSNodegroupName, r.Spec.EKSNodegroupName, "eksNodegroupName") + if old.EKSNodegroupName != "" { + appendErrorIfMutated(old.EKSNodegroupName, current.EKSNodegroupName, "eksNodegroupName") } - appendErrorIfMutated(old.Spec.SubnetIDs, r.Spec.SubnetIDs, "subnetIDs") - appendErrorIfSetAndMutated(old.Spec.RoleName, r.Spec.RoleName, "roleName") - appendErrorIfMutated(old.Spec.DiskSize, r.Spec.DiskSize, "diskSize") - appendErrorIfMutated(old.Spec.AMIType, r.Spec.AMIType, "amiType") - appendErrorIfMutated(old.Spec.RemoteAccess, r.Spec.RemoteAccess, "remoteAccess") - appendErrorIfSetAndMutated(old.Spec.CapacityType, r.Spec.CapacityType, "capacityType") - appendErrorIfMutated(old.Spec.AvailabilityZones, r.Spec.AvailabilityZones, "availabilityZones") - appendErrorIfMutated(old.Spec.AvailabilityZoneSubnetType, r.Spec.AvailabilityZoneSubnetType, "availabilityZoneSubnetType") - if (old.Spec.AWSLaunchTemplate != nil && r.Spec.AWSLaunchTemplate == nil) || - (old.Spec.AWSLaunchTemplate == nil && r.Spec.AWSLaunchTemplate != nil) { + appendErrorIfMutated(old.SubnetIDs, current.SubnetIDs, "subnetIDs") + appendErrorIfSetAndMutated(old.RoleName, current.RoleName, "roleName") + appendErrorIfMutated(old.DiskSize, current.DiskSize, "diskSize") + appendErrorIfMutated(old.AMIType, current.AMIType, "amiType") + appendErrorIfMutated(old.RemoteAccess, current.RemoteAccess, "remoteAccess") + appendErrorIfSetAndMutated(old.CapacityType, current.CapacityType, "capacityType") + appendErrorIfMutated(old.AvailabilityZones, current.AvailabilityZones, "availabilityZones") + appendErrorIfMutated(old.AvailabilityZoneSubnetType, current.AvailabilityZoneSubnetType, "availabilityZoneSubnetType") + if (old.AWSLaunchTemplate != nil && current.AWSLaunchTemplate == nil) || + (old.AWSLaunchTemplate == nil && current.AWSLaunchTemplate != nil) { allErrs = append( allErrs, - field.Invalid(field.NewPath("spec", "AWSLaunchTemplate"), old.Spec.AWSLaunchTemplate, "field is immutable"), + field.Invalid(field.NewPath("spec", "AWSLaunchTemplate"), old.AWSLaunchTemplate, "field is immutable"), ) } - if old.Spec.AWSLaunchTemplate != nil && r.Spec.AWSLaunchTemplate != nil { - appendErrorIfMutated(old.Spec.AWSLaunchTemplate.Name, r.Spec.AWSLaunchTemplate.Name, "awsLaunchTemplate.name") + if old.AWSLaunchTemplate != nil && current.AWSLaunchTemplate != nil { + appendErrorIfMutated(old.AWSLaunchTemplate.Name, current.AWSLaunchTemplate.Name, "awsLaunchTemplate.name") } return allErrs @@ -306,9 +306,13 @@ func (*awsManagedMachinePoolWebhook) Default(_ context.Context, obj runtime.Obje } if r.Spec.UpdateConfig == nil { - r.Spec.UpdateConfig = &UpdateConfig{ - MaxUnavailable: ptr.To[int](1), - } + r.Spec.UpdateConfig = defaultManagedMachinePoolUpdateConfig() } return nil } + +func defaultManagedMachinePoolUpdateConfig() *UpdateConfig { + return &UpdateConfig{ + MaxUnavailable: ptr.To[int](1), + } +} diff --git a/exp/api/v1beta2/awsmanagedmachinepooltemplate_types.go b/exp/api/v1beta2/awsmanagedmachinepooltemplate_types.go new file mode 100644 index 0000000000..cfae3a21c6 --- /dev/null +++ b/exp/api/v1beta2/awsmanagedmachinepooltemplate_types.go @@ -0,0 +1,56 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:resource:path=awsmanagedmachinepooltemplates,scope=Namespaced,categories=cluster-api,shortName=awsmmpt + +// AWSManagedMachinePoolTemplate is the Schema for the awsmanagedmachinepooltemplates API. +type AWSManagedMachinePoolTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AWSManagedMachinePoolTemplateSpec `json:"spec,omitempty"` +} + +// AWSManagedMachinePoolTemplateResource wraps AWSManagedMachinePoolSpec +type AWSManagedMachinePoolTemplateResource struct { + Spec AWSManagedMachinePoolSpec `json:"spec"` +} + +// AWSManagedMachinePoolTemplateSpec defines the desired state of AWSManagedMachinePoolTemplate. +type AWSManagedMachinePoolTemplateSpec struct { + Template *AWSManagedMachinePoolTemplateResource `json:"template"` +} + +// +kubebuilder:object:root=true + +// AWSManagedMachinePoolTemplateList contains a list of AWSManagedMachinePoolTemplates. +type AWSManagedMachinePoolTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AWSManagedMachinePoolTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AWSManagedMachinePoolTemplate{}, &AWSManagedMachinePoolTemplateList{}) +} diff --git a/exp/api/v1beta2/awsmanagedmachinepooltemplate_webhook.go b/exp/api/v1beta2/awsmanagedmachinepooltemplate_webhook.go new file mode 100644 index 0000000000..a8aabb3820 --- /dev/null +++ b/exp/api/v1beta2/awsmanagedmachinepooltemplate_webhook.go @@ -0,0 +1,146 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// SetupWebhookWithManager will setup the webhooks for the AWSManagedMachinePool. +func (rt *AWSManagedMachinePoolTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { + w := new(awsManagedMachinePoolTemplateWebhook) + return ctrl.NewWebhookManagedBy(mgr). + For(rt). + WithValidator(w). + WithDefaulter(w). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta2-awsmanagedmachinepooltemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmanagedmachinepooltemplates,versions=v1beta2,name=validation.awsmanagedmachinepooltemplate.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta2-awsmanagedmachinepooltemplate,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=awsmanagedmachinepooltemplates,versions=v1beta2,name=default.awsmanagedmachinepooltemplate.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +type awsManagedMachinePoolTemplateWebhook struct{} + +var _ webhook.CustomDefaulter = &awsManagedMachinePoolTemplateWebhook{} +var _ webhook.CustomValidator = &awsManagedMachinePoolTemplateWebhook{} + +// ValidateCreate will do any extra validation when creating a AWSManagedMachinePoolTemplate. +func (*awsManagedMachinePoolTemplateWebhook) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*AWSManagedMachinePoolTemplate) + if !ok { + return nil, fmt.Errorf("expected an AWSManagedMachinePoolTemplate object but got %T", r) + } + mmpLog.Info("AWSManagedMachinePoolTemplate validate create", "managed-machine-pool", klog.KObj(r)) + + var allErrs field.ErrorList + + if errs := validateScaling(&r.Spec.Template.Spec); errs != nil || len(errs) == 0 { + allErrs = append(allErrs, errs...) + } + + if errs := validateRemoteAccess(&r.Spec.Template.Spec); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + + if errs := validateNodegroupUpdateConfig(&r.Spec.Template.Spec); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + + if errs := validateLaunchTemplate(&r.Spec.Template.Spec); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + + allErrs = append(allErrs, r.Spec.Template.Spec.AdditionalTags.Validate()...) + + if len(allErrs) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) +} + +// ValidateUpdate will do any extra validation when creating a AWSManagedMachinePoolTemplate. +func (*awsManagedMachinePoolTemplateWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + r, ok := newObj.(*AWSManagedMachinePoolTemplate) + if !ok { + return nil, fmt.Errorf("expected an AWSManagedMachinePoolTemplate object but got %T", r) + } + + mmpLog.Info("AWSManagedMachinePoolTemplate validate update", "managed-machine-pool", klog.KObj(r)) + + mmpLog.Info("AWSManagedMachinePool validate update", "managed-machine-pool", klog.KObj(r)) + oldPool, ok := oldObj.(*AWSManagedMachinePoolTemplate) + if !ok { + return nil, apierrors.NewInvalid(GroupVersion.WithKind("AWSManagedMachinePool").GroupKind(), r.Name, field.ErrorList{ + field.InternalError(nil, errors.New("failed to convert old AWSManagedMachinePool to object")), + }) + } + + var allErrs field.ErrorList + allErrs = append(allErrs, validateAMPImmutable(&oldPool.Spec.Template.Spec, &r.Spec.Template.Spec)...) + allErrs = append(allErrs, r.Spec.Template.Spec.AdditionalTags.Validate()...) + + if errs := validateScaling(&r.Spec.Template.Spec); errs != nil || len(errs) == 0 { + allErrs = append(allErrs, errs...) + } + if errs := validateNodegroupUpdateConfig(&r.Spec.Template.Spec); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + if errs := validateLaunchTemplate(&r.Spec.Template.Spec); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + + if len(allErrs) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) +} + +// ValidateDelete will do any extra validation when creating a AWSManagedMachinePoolTemplate. +func (*awsManagedMachinePoolTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// Default will set default values for the AWSManagedMachinePool. +func (*awsManagedMachinePoolTemplateWebhook) Default(_ context.Context, obj runtime.Object) error { + r, ok := obj.(*AWSManagedMachinePoolTemplate) + if !ok { + return fmt.Errorf("expected an AWSManagedMachinePoolTemplate object but got %T", r) + } + if r.Spec.Template.Spec.UpdateConfig == nil { + r.Spec.Template.Spec.UpdateConfig = defaultManagedMachinePoolUpdateConfig() + } + return nil +} diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go index af59aa3b75..ae1b6abfa0 100644 --- a/exp/api/v1beta2/zz_generated.deepcopy.go +++ b/exp/api/v1beta2/zz_generated.deepcopy.go @@ -623,6 +623,100 @@ func (in *AWSManagedMachinePoolStatus) DeepCopy() *AWSManagedMachinePoolStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedMachinePoolTemplate) DeepCopyInto(out *AWSManagedMachinePoolTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedMachinePoolTemplate. +func (in *AWSManagedMachinePoolTemplate) DeepCopy() *AWSManagedMachinePoolTemplate { + if in == nil { + return nil + } + out := new(AWSManagedMachinePoolTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedMachinePoolTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedMachinePoolTemplateList) DeepCopyInto(out *AWSManagedMachinePoolTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AWSManagedMachinePoolTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedMachinePoolTemplateList. +func (in *AWSManagedMachinePoolTemplateList) DeepCopy() *AWSManagedMachinePoolTemplateList { + if in == nil { + return nil + } + out := new(AWSManagedMachinePoolTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedMachinePoolTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedMachinePoolTemplateResource) DeepCopyInto(out *AWSManagedMachinePoolTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedMachinePoolTemplateResource. +func (in *AWSManagedMachinePoolTemplateResource) DeepCopy() *AWSManagedMachinePoolTemplateResource { + if in == nil { + return nil + } + out := new(AWSManagedMachinePoolTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedMachinePoolTemplateSpec) DeepCopyInto(out *AWSManagedMachinePoolTemplateSpec) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(AWSManagedMachinePoolTemplateResource) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedMachinePoolTemplateSpec. +func (in *AWSManagedMachinePoolTemplateSpec) DeepCopy() *AWSManagedMachinePoolTemplateSpec { + if in == nil { + return nil + } + out := new(AWSManagedMachinePoolTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AccountRoleConfig) DeepCopyInto(out *AccountRoleConfig) { *out = *in diff --git a/main.go b/main.go index 785d1e7969..3b9f96f003 100644 --- a/main.go +++ b/main.go @@ -381,6 +381,10 @@ func setupReconcilersAndWebhooks(ctx context.Context, mgr ctrl.Manager, setupLog.Error(err, "unable to create webhook", "webhook", "AWSMachinePool") os.Exit(1) } + if err := (&expinfrav1.AWSManagedMachinePoolTemplate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AWSManagedMachinePoolTemplate") + os.Exit(1) + } } if feature.Gates.Enabled(feature.EventBridgeInstanceState) {