diff --git a/api/v1beta2/awsmanagedclustertemplate_types.go b/api/v1beta2/awsmanagedclustertemplate_types.go new file mode 100644 index 0000000000..266ccf400f --- /dev/null +++ b/api/v1beta2/awsmanagedclustertemplate_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" +) + +// AWSManagedClusterTemplateSpec defines the desired state of AWSManagedClusterTemplate. +type AWSManagedClusterTemplateSpec struct { + Template AWSManagedClusterTemplateResource `json:"template"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=awsmanagedclustertemplates,scope=Namespaced,categories=cluster-api,shortName=amct +// +kubebuilder:storageversion + +// AWSManagedClusterTemplate is the Schema for the AWSManagedClusterTemplates API. +type AWSManagedClusterTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AWSManagedClusterTemplateSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// AWSManagedClusterTemplateList contains a list of AWSManagedClusterTemplates. +type AWSManagedClusterTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AWSManagedClusterTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AWSManagedClusterTemplate{}, &AWSManagedClusterTemplateList{}) +} + +// AWSManagedClusterTemplateResource describes the data needed to create an AWSManagedCluster from a template. +type AWSManagedClusterTemplateResource struct { + Spec AWSManagedClusterSpec `json:"spec"` +} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 7d39649cfa..1bfe5ddc46 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -1043,6 +1043,96 @@ func (in *AWSManagedClusterStatus) DeepCopy() *AWSManagedClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedClusterTemplate) DeepCopyInto(out *AWSManagedClusterTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedClusterTemplate. +func (in *AWSManagedClusterTemplate) DeepCopy() *AWSManagedClusterTemplate { + if in == nil { + return nil + } + out := new(AWSManagedClusterTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedClusterTemplate) 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 *AWSManagedClusterTemplateList) DeepCopyInto(out *AWSManagedClusterTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AWSManagedClusterTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedClusterTemplateList. +func (in *AWSManagedClusterTemplateList) DeepCopy() *AWSManagedClusterTemplateList { + if in == nil { + return nil + } + out := new(AWSManagedClusterTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedClusterTemplateList) 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 *AWSManagedClusterTemplateResource) DeepCopyInto(out *AWSManagedClusterTemplateResource) { + *out = *in + out.Spec = in.Spec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedClusterTemplateResource. +func (in *AWSManagedClusterTemplateResource) DeepCopy() *AWSManagedClusterTemplateResource { + if in == nil { + return nil + } + out := new(AWSManagedClusterTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedClusterTemplateSpec) DeepCopyInto(out *AWSManagedClusterTemplateSpec) { + *out = *in + out.Template = in.Template +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedClusterTemplateSpec. +func (in *AWSManagedClusterTemplateSpec) DeepCopy() *AWSManagedClusterTemplateSpec { + if in == nil { + return nil + } + out := new(AWSManagedClusterTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AWSResourceReference) DeepCopyInto(out *AWSResourceReference) { *out = *in diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml new file mode 100644 index 0000000000..369829fbb1 --- /dev/null +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml @@ -0,0 +1,1172 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io +spec: + group: controlplane.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: AWSManagedControlPlaneTemplate + listKind: AWSManagedControlPlaneTemplateList + plural: awsmanagedcontrolplanetemplates + shortNames: + - awmcpt + singular: awsmanagedcontrolplanetemplate + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: AWSManagedControlPlaneTemplate is the Schema for the AWSManagedControlPlaneTemplates + 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: AWSManagedControlPlaneTemplateSpec defines the desired state + of AWSManagedControlPlaneTemplate. + properties: + template: + description: AWSManagedControlPlaneTemplateResource describes the + data needed to create an AWSManagedCluster from a template. + properties: + spec: + description: AWSManagedControlPlaneSpec defines the desired state + of an Amazon EKS Cluster. + 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 + addons: + description: Addons defines the EKS addons to enable with + the EKS cluster. + items: + description: Addon represents a EKS addon. + properties: + configuration: + description: Configuration of the EKS addon + type: string + conflictResolution: + default: overwrite + description: |- + ConflictResolution is used to declare what should happen if there + are parameter conflicts. Defaults to none + enum: + - overwrite + - none + type: string + name: + description: Name is the name of the addon + minLength: 2 + type: string + serviceAccountRoleARN: + description: ServiceAccountRoleArn is the ARN of an + IAM role to bind to the addons service account + type: string + version: + description: Version is the version of the addon to + use + type: string + required: + - name + - version + type: object + type: array + associateOIDCProvider: + default: false + description: |- + AssociateOIDCProvider can be enabled to automatically create an identity + provider for the controller for use with IAM roles for service accounts + type: boolean + bastion: + description: Bastion contains options to configure the bastion + host. + properties: + allowedCIDRBlocks: + description: |- + AllowedCIDRBlocks is a list of CIDR blocks allowed to access the bastion host. + They are set as ingress rules for the Bastion host's Security Group (defaults to 0.0.0.0/0). + items: + type: string + type: array + ami: + description: |- + AMI will use the specified AMI to boot the bastion. If not specified, + the AMI will default to one picked out in public space. + type: string + disableIngressRules: + description: |- + DisableIngressRules will ensure there are no Ingress rules in the bastion host's security group. + Requires AllowedCIDRBlocks to be empty. + type: boolean + enabled: + description: |- + Enabled allows this provider to create a bastion host instance + with a public ip to access the VPC private network. + type: boolean + instanceType: + description: |- + InstanceType will use the specified instance type for the bastion. If not specified, + Cluster API Provider AWS will use t3.micro for all regions except us-east-1, where t2.micro + will be the default. + type: string + type: object + bootstrapSelfManagedAddons: + default: true + description: |- + BootstrapSelfManagedAddons is used to set configuration options for + bare EKS cluster without EKS default networking addons + If you set this value to false when creating a cluster, the default networking add-ons will not be installed + type: boolean + controlPlaneEndpoint: + description: ControlPlaneEndpoint represents the endpoint + used to communicate with the control plane. + properties: + host: + description: host is the hostname on which the API server + is serving. + maxLength: 512 + type: string + port: + description: port is the port on which the API server + is serving. + format: int32 + type: integer + required: + - host + - port + type: object + eksClusterName: + description: |- + EKSClusterName allows you to specify the name of the EKS cluster in + AWS. If you don't specify a name then a default name will be created + based on the namespace and name of the managed control plane. + type: string + encryptionConfig: + description: EncryptionConfig specifies the encryption configuration + for the cluster + properties: + provider: + description: Provider specifies the ARN or alias of the + CMK (in AWS KMS) + type: string + resources: + description: Resources specifies the resources to be encrypted + items: + type: string + type: array + type: object + endpointAccess: + description: Endpoints specifies access to this cluster's + control plane endpoints + properties: + private: + description: Private points VPC-internal control plane + access to the private endpoint + type: boolean + public: + description: Public controls whether control plane endpoints + are publicly accessible + type: boolean + publicCIDRs: + description: PublicCIDRs specifies which blocks can access + the public endpoint + items: + type: string + type: array + type: object + iamAuthenticatorConfig: + description: |- + IAMAuthenticatorConfig allows the specification of any additional user or role mappings + for use when generating the aws-iam-authenticator configuration. If this is nil the + default configuration is still generated for the cluster. + properties: + mapRoles: + description: RoleMappings is a list of role mappings + items: + description: RoleMapping represents a mapping from a + IAM role to Kubernetes users and groups. + properties: + groups: + description: Groups is a list of kubernetes RBAC + groups + items: + type: string + type: array + rolearn: + description: RoleARN is the AWS ARN for the role + to map + minLength: 31 + type: string + username: + description: UserName is a kubernetes RBAC user + subject + type: string + required: + - groups + - rolearn + - username + type: object + type: array + mapUsers: + description: UserMappings is a list of user mappings + items: + description: UserMapping represents a mapping from an + IAM user to Kubernetes users and groups. + properties: + groups: + description: Groups is a list of kubernetes RBAC + groups + items: + type: string + type: array + userarn: + description: UserARN is the AWS ARN for the user + to map + minLength: 31 + type: string + username: + description: UserName is a kubernetes RBAC user + subject + type: string + required: + - groups + - userarn + - username + type: object + type: array + type: object + identityRef: + description: |- + IdentityRef is a reference to an identity to be used when reconciling the managed control plane. + If no identity is specified, the default identity for this controller will be used. + properties: + kind: + description: Kind of the identity. + enum: + - AWSClusterControllerIdentity + - AWSClusterRoleIdentity + - AWSClusterStaticIdentity + type: string + name: + description: Name of the identity. + minLength: 1 + type: string + required: + - kind + - name + type: object + imageLookupBaseOS: + description: |- + ImageLookupBaseOS is the name of the base operating system used to look + up machine images when a machine does not specify an AMI. When set, this + will be used for all cluster machines unless a machine specifies a + different ImageLookupBaseOS. + type: string + imageLookupFormat: + description: |- + ImageLookupFormat is the AMI naming format to look up machine images when + a machine does not specify an AMI. When set, this will be used for all + cluster machines unless a machine specifies a different ImageLookupOrg. + 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 look up machine images when a + machine does not specify an AMI. When set, this will be used for all + cluster machines unless a machine specifies a different ImageLookupOrg. + type: string + kubeProxy: + description: KubeProxy defines managed attributes of the kube-proxy + daemonset + properties: + disable: + default: false + description: |- + Disable set to true indicates that kube-proxy should be disabled. With EKS clusters + kube-proxy is automatically installed into the cluster. For clusters where you want + to use kube-proxy functionality that is provided with an alternate CNI, this option + provides a way to specify that the kube-proxy daemonset should be deleted. You cannot + set this to true if you are using the Amazon kube-proxy addon. + type: boolean + type: object + logging: + description: |- + Logging specifies which EKS Cluster logs should be enabled. Entries for + each of the enabled logs will be sent to CloudWatch + properties: + apiServer: + default: false + description: APIServer indicates if the Kubernetes API + Server log (kube-apiserver) shoulkd be enabled + type: boolean + audit: + default: false + description: Audit indicates if the Kubernetes API audit + log should be enabled + type: boolean + authenticator: + default: false + description: Authenticator indicates if the iam authenticator + log should be enabled + type: boolean + controllerManager: + default: false + description: ControllerManager indicates if the controller + manager (kube-controller-manager) log should be enabled + type: boolean + scheduler: + default: false + description: Scheduler indicates if the Kubernetes scheduler + (kube-scheduler) log should be enabled + type: boolean + required: + - apiServer + - audit + - authenticator + - controllerManager + - scheduler + type: object + network: + description: NetworkSpec encapsulates all things related to + AWS network. + properties: + additionalControlPlaneIngressRules: + description: AdditionalControlPlaneIngressRules is an + optional set of ingress rules to add to the control + plane + items: + description: IngressRule defines an AWS ingress rule + for security groups. + properties: + cidrBlocks: + description: List of CIDR blocks to allow access + from. Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + description: + description: Description provides extended information + about the ingress rule. + type: string + fromPort: + description: FromPort is the start of port range. + format: int64 + type: integer + ipv6CidrBlocks: + description: List of IPv6 CIDR blocks to allow access + from. Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + natGatewaysIPsSource: + description: NatGatewaysIPsSource use the NAT gateways + IPs as the source for the ingress rule. + type: boolean + protocol: + description: Protocol is the protocol for the ingress + rule. Accepted values are "-1" (all), "4" (IP + in IP),"tcp", "udp", "icmp", and "58" (ICMPv6), + "50" (ESP). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" + - "50" + type: string + sourceSecurityGroupIds: + description: The security group id to allow access + from. Cannot be specified with CidrBlocks. + items: + type: string + type: array + sourceSecurityGroupRoles: + description: |- + The security group role to allow access from. Cannot be specified with CidrBlocks. + The field will be combined with source security group IDs if specified. + items: + description: SecurityGroupRole defines the unique + role of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array + toPort: + description: ToPort is the end of port range. + format: int64 + type: integer + required: + - description + - fromPort + - protocol + - toPort + type: object + type: array + additionalNodeIngressRules: + description: AdditionalNodeIngressRules is an optional + set of ingress rules to add to every node + items: + description: IngressRule defines an AWS ingress rule + for security groups. + properties: + cidrBlocks: + description: List of CIDR blocks to allow access + from. Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + description: + description: Description provides extended information + about the ingress rule. + type: string + fromPort: + description: FromPort is the start of port range. + format: int64 + type: integer + ipv6CidrBlocks: + description: List of IPv6 CIDR blocks to allow access + from. Cannot be specified with SourceSecurityGroupID. + items: + type: string + type: array + natGatewaysIPsSource: + description: NatGatewaysIPsSource use the NAT gateways + IPs as the source for the ingress rule. + type: boolean + protocol: + description: Protocol is the protocol for the ingress + rule. Accepted values are "-1" (all), "4" (IP + in IP),"tcp", "udp", "icmp", and "58" (ICMPv6), + "50" (ESP). + enum: + - "-1" + - "4" + - tcp + - udp + - icmp + - "58" + - "50" + type: string + sourceSecurityGroupIds: + description: The security group id to allow access + from. Cannot be specified with CidrBlocks. + items: + type: string + type: array + sourceSecurityGroupRoles: + description: |- + The security group role to allow access from. Cannot be specified with CidrBlocks. + The field will be combined with source security group IDs if specified. + items: + description: SecurityGroupRole defines the unique + role of a security group. + enum: + - bastion + - node + - controlplane + - apiserver-lb + - lb + - node-eks-additional + type: string + type: array + toPort: + description: ToPort is the end of port range. + format: int64 + type: integer + required: + - description + - fromPort + - protocol + - toPort + type: object + type: array + cni: + description: CNI configuration + properties: + cniIngressRules: + description: |- + CNIIngressRules specify rules to apply to control plane and worker node security groups. + The source for the rule will be set to control plane and worker security group IDs. + items: + description: CNIIngressRule defines an AWS ingress + rule for CNI requirements. + properties: + description: + type: string + fromPort: + format: int64 + type: integer + protocol: + description: SecurityGroupProtocol defines the + protocol type for a security group rule. + type: string + toPort: + format: int64 + type: integer + required: + - description + - fromPort + - protocol + - toPort + type: object + type: array + type: object + nodePortIngressRuleCidrBlocks: + description: |- + NodePortIngressRuleCidrBlocks is an optional set of CIDR blocks to allow traffic to nodes' NodePort services. + If none are specified here, all IPs are allowed to connect. + items: + type: string + type: array + securityGroupOverrides: + additionalProperties: + type: string + description: |- + SecurityGroupOverrides is an optional set of security groups to use for cluster instances + This is optional - if not provided new security groups will be created for the cluster + type: object + subnets: + description: Subnets configuration. + items: + description: SubnetSpec configures an AWS Subnet. + properties: + availabilityZone: + description: AvailabilityZone defines the availability + zone to use for this subnet in the cluster's region. + type: string + cidrBlock: + description: CidrBlock is the CIDR block to be used + when the provider creates a managed VPC. + type: string + id: + description: |- + ID defines a unique identifier to reference this resource. + If you're bringing your subnet, set the AWS subnet-id here, it must start with `subnet-`. + + When the VPC is managed by CAPA, and you'd like the provider to create a subnet for you, + the id can be set to any placeholder value that does not start with `subnet-`; + upon creation, the subnet AWS identifier will be populated in the `ResourceID` field and + the `id` field is going to be used as the subnet name. If you specify a tag + called `Name`, it takes precedence. + type: string + ipv6CidrBlock: + description: |- + IPv6CidrBlock is the IPv6 CIDR block to be used when the provider creates a managed VPC. + A subnet can have an IPv4 and an IPv6 address. + IPv6 is only supported in managed clusters, this field cannot be set on AWSCluster object. + type: string + isIpv6: + description: |- + IsIPv6 defines the subnet as an IPv6 subnet. A subnet is IPv6 when it is associated with a VPC that has IPv6 enabled. + IPv6 is only supported in managed clusters, this field cannot be set on AWSCluster object. + type: boolean + isPublic: + description: IsPublic defines the subnet as a public + subnet. A subnet is public when it is associated + with a route table that has a route to an internet + gateway. + type: boolean + natGatewayId: + description: |- + NatGatewayID is the NAT gateway id associated with the subnet. + Ignored unless the subnet is managed by the provider, in which case this is set on the public subnet where the NAT gateway resides. It is then used to determine routes for private subnets in the same AZ as the public subnet. + type: string + parentZoneName: + description: |- + ParentZoneName is the zone name where the current subnet's zone is tied when + the zone is a Local Zone. + + The subnets in Local Zone or Wavelength Zone locations consume the ParentZoneName + to select the correct private route table to egress traffic to the internet. + type: string + resourceID: + description: |- + ResourceID is the subnet identifier from AWS, READ ONLY. + This field is populated when the provider manages the subnet. + type: string + routeTableId: + description: RouteTableID is the routing table id + associated with the subnet. + type: string + tags: + additionalProperties: + type: string + description: Tags is a collection of tags describing + the resource. + type: object + zoneType: + description: |- + ZoneType defines the type of the zone where the subnet is created. + + The valid values are availability-zone, local-zone, and wavelength-zone. + + Subnet with zone type availability-zone (regular) is always selected to create cluster + resources, like Load Balancers, NAT Gateways, Contol Plane nodes, etc. + + Subnet with zone type local-zone or wavelength-zone is not eligible to automatically create + regular cluster resources. + + The public subnet in availability-zone or local-zone is associated with regular public + route table with default route entry to a Internet Gateway. + + The public subnet in wavelength-zone is associated with a carrier public + route table with default route entry to a Carrier Gateway. + + The private subnet in the availability-zone is associated with a private route table with + the default route entry to a NAT Gateway created in that zone. + + The private subnet in the local-zone or wavelength-zone is associated with a private route table with + the default route entry re-using the NAT Gateway in the Region (preferred from the + parent zone, the zone type availability-zone in the region, or first table available). + enum: + - availability-zone + - local-zone + - wavelength-zone + type: string + required: + - id + type: object + type: array + x-kubernetes-list-map-keys: + - id + x-kubernetes-list-type: map + vpc: + description: VPC configuration. + properties: + availabilityZoneSelection: + default: Ordered + description: |- + AvailabilityZoneSelection specifies how AZs should be selected if there are more AZs + in a region than specified by AvailabilityZoneUsageLimit. There are 2 selection schemes: + Ordered - selects based on alphabetical order + Random - selects AZs randomly in a region + Defaults to Ordered + enum: + - Ordered + - Random + type: string + availabilityZoneUsageLimit: + default: 3 + description: |- + AvailabilityZoneUsageLimit specifies the maximum number of availability zones (AZ) that + should be used in a region when automatically creating subnets. If a region has more + than this number of AZs then this number of AZs will be picked randomly when creating + default subnets. Defaults to 3 + minimum: 1 + type: integer + carrierGatewayId: + description: |- + CarrierGatewayID is the id of the internet gateway associated with the VPC, + for carrier network (Wavelength Zones). + type: string + x-kubernetes-validations: + - message: Carrier Gateway ID must start with 'cagw-' + rule: self.startsWith('cagw-') + cidrBlock: + description: |- + CidrBlock is the CIDR block to be used when the provider creates a managed VPC. + Defaults to 10.0.0.0/16. + Mutually exclusive with IPAMPool. + type: string + elasticIpPool: + description: |- + ElasticIPPool contains specific configuration to allocate Public IPv4 address (Elastic IP) from user-defined pool + brought to AWS for core infrastructure resources, like NAT Gateways and Public Network Load Balancers for + the API Server. + properties: + publicIpv4Pool: + description: |- + PublicIpv4Pool sets a custom Public IPv4 Pool used to create Elastic IP address for resources + created in public IPv4 subnets. Every IPv4 address, Elastic IP, will be allocated from the custom + Public IPv4 pool that you brought to AWS, instead of Amazon-provided pool. The public IPv4 pool + resource ID starts with 'ipv4pool-ec2'. + maxLength: 30 + type: string + publicIpv4PoolFallbackOrder: + description: |- + PublicIpv4PoolFallBackOrder defines the fallback action when the Public IPv4 Pool has been exhausted, + no more IPv4 address available in the pool. + + When set to 'amazon-pool', the controller check if the pool has available IPv4 address, when pool has reached the + IPv4 limit, the address will be claimed from Amazon-pool (default). + + When set to 'none', the controller will fail the Elastic IP allocation when the publicIpv4Pool is exhausted. + enum: + - amazon-pool + - none + type: string + x-kubernetes-validations: + - message: allowed values are 'none' and 'amazon-pool' + rule: self in ['none','amazon-pool'] + type: object + emptyRoutesDefaultVPCSecurityGroup: + description: |- + EmptyRoutesDefaultVPCSecurityGroup specifies whether the default VPC security group ingress + and egress rules should be removed. + + By default, when creating a VPC, AWS creates a security group called `default` with ingress and egress + rules that allow traffic from anywhere. The group could be used as a potential surface attack and + it's generally suggested that the group rules are removed or modified appropriately. + + NOTE: This only applies when the VPC is managed by the Cluster API AWS controller. + type: boolean + id: + description: ID is the vpc-id of the VPC this provider + should use to create resources. + type: string + internetGatewayId: + description: InternetGatewayID is the id of the internet + gateway associated with the VPC. + type: string + ipamPool: + description: |- + IPAMPool defines the IPAMv4 pool to be used for VPC. + Mutually exclusive with CidrBlock. + properties: + id: + description: ID is the ID of the IPAM pool this + provider should use to create VPC. + type: string + name: + description: Name is the name of the IPAM pool + this provider should use to create VPC. + type: string + netmaskLength: + description: |- + The netmask length of the IPv4 CIDR you want to allocate to VPC from + an Amazon VPC IP Address Manager (IPAM) pool. + Defaults to /16 for IPv4 if not specified. + format: int64 + type: integer + type: object + ipv6: + description: |- + IPv6 contains ipv6 specific settings for the network. Supported only in managed clusters. + This field cannot be set on AWSCluster object. + properties: + cidrBlock: + description: |- + CidrBlock is the CIDR block provided by Amazon when VPC has enabled IPv6. + Mutually exclusive with IPAMPool. + type: string + egressOnlyInternetGatewayId: + description: EgressOnlyInternetGatewayID is the + id of the egress only internet gateway associated + with an IPv6 enabled VPC. + type: string + ipamPool: + description: |- + IPAMPool defines the IPAMv6 pool to be used for VPC. + Mutually exclusive with CidrBlock. + properties: + id: + description: ID is the ID of the IPAM pool + this provider should use to create VPC. + type: string + name: + description: Name is the name of the IPAM + pool this provider should use to create + VPC. + type: string + netmaskLength: + description: |- + The netmask length of the IPv4 CIDR you want to allocate to VPC from + an Amazon VPC IP Address Manager (IPAM) pool. + Defaults to /16 for IPv4 if not specified. + format: int64 + type: integer + type: object + poolId: + description: |- + PoolID is the IP pool which must be defined in case of BYO IP is defined. + Must be specified if CidrBlock is set. + Mutually exclusive with IPAMPool. + type: string + type: object + privateDnsHostnameTypeOnLaunch: + description: |- + PrivateDNSHostnameTypeOnLaunch is the type of hostname to assign to instances in the subnet at launch. + For IPv4-only and dual-stack (IPv4 and IPv6) subnets, an instance DNS name can be based on the instance IPv4 address (ip-name) + or the instance ID (resource-name). For IPv6 only subnets, an instance DNS name must be based on the instance ID (resource-name). + enum: + - ip-name + - resource-name + type: string + secondaryCidrBlocks: + description: |- + SecondaryCidrBlocks are additional CIDR blocks to be associated when the provider creates a managed VPC. + Defaults to none. Mutually exclusive with IPAMPool. This makes sense to use if, for example, you want to use + a separate IP range for pods (e.g. Cilium ENI mode). + items: + description: VpcCidrBlock defines the CIDR block + and settings to associate with the managed VPC. + Currently, only IPv4 is supported. + properties: + ipv4CidrBlock: + description: IPv4CidrBlock is the IPv4 CIDR + block to associate with the managed VPC. + minLength: 1 + type: string + required: + - ipv4CidrBlock + type: object + type: array + subnetSchema: + default: PreferPrivate + description: |- + SubnetSchema specifies how CidrBlock should be divided on subnets in the VPC depending on the number of AZs. + PreferPrivate - one private subnet for each AZ plus one other subnet that will be further sub-divided for the public subnets. + PreferPublic - have the reverse logic of PreferPrivate, one public subnet for each AZ plus one other subnet + that will be further sub-divided for the private subnets. + Defaults to PreferPrivate + enum: + - PreferPrivate + - PreferPublic + type: string + tags: + additionalProperties: + type: string + description: Tags is a collection of tags describing + the resource. + type: object + type: object + type: object + oidcIdentityProviderConfig: + description: |- + IdentityProviderconfig is used to specify the oidc provider config + to be attached with this eks cluster + properties: + clientId: + description: |- + This is also known as audience. The ID for the client application that makes + authentication requests to the OpenID identity provider. + type: string + groupsClaim: + description: The JWT claim that the provider uses to return + your groups. + type: string + groupsPrefix: + description: |- + The prefix that is prepended to group claims to prevent clashes with existing + names (such as system: groups). For example, the valueoidc: will create group + names like oidc:engineering and oidc:infra. + type: string + identityProviderConfigName: + description: |- + The name of the OIDC provider configuration. + + IdentityProviderConfigName is a required field + type: string + issuerUrl: + description: |- + The URL of the OpenID identity provider that allows the API server to discover + public signing keys for verifying tokens. The URL must begin with https:// + and should correspond to the iss claim in the provider's OIDC ID tokens. + Per the OIDC standard, path components are allowed but query parameters are + not. Typically the URL consists of only a hostname, like https://server.example.org + or https://example.com. This URL should point to the level below .well-known/openid-configuration + and must be publicly accessible over the internet. + type: string + requiredClaims: + additionalProperties: + type: string + description: |- + The key value pairs that describe required claims in the identity token. + If set, each claim is verified to be present in the token with a matching + value. For the maximum number of claims that you can require, see Amazon + EKS service quotas (https://docs.aws.amazon.com/eks/latest/userguide/service-quotas.html) + in the Amazon EKS User Guide. + type: object + tags: + additionalProperties: + type: string + description: tags to apply to oidc identity provider association + type: object + usernameClaim: + description: |- + The JSON Web Token (JWT) claim to use as the username. The default is sub, + which is expected to be a unique identifier of the end user. You can choose + other claims, such as email or name, depending on the OpenID identity provider. + Claims other than email are prefixed with the issuer URL to prevent naming + clashes with other plug-ins. + type: string + usernamePrefix: + description: |- + The prefix that is prepended to username claims to prevent clashes with existing + names. If you do not provide this field, and username is a value other than + email, the prefix defaults to issuerurl#. You can use the value - to disable + all prefixing. + type: string + required: + - clientId + - identityProviderConfigName + - issuerUrl + type: object + partition: + description: Partition is the AWS security partition being + used. Defaults to "aws" + type: string + region: + description: The AWS Region the cluster lives in. + type: string + restrictPrivateSubnets: + default: false + description: RestrictPrivateSubnets indicates that the EKS + control plane should only use private subnets. + type: boolean + roleAdditionalPolicies: + description: |- + RoleAdditionalPolicies allows you to attach additional polices to + the control plane 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 that gives EKS + permission to make API calls. 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. + minLength: 2 + 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 + secondaryCidrBlock: + description: |- + SecondaryCidrBlock is the additional CIDR range to use for pod IPs. + Must be within the 100.64.0.0/10 or 198.19.0.0/16 range. + type: string + sshKeyName: + description: SSHKeyName is the name of the ssh key to attach + to the bastion host. 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 + tokenMethod: + default: iam-authenticator + description: |- + TokenMethod is used to specify the method for obtaining a client token for communicating with EKS + iam-authenticator - obtains a client token using iam-authentictor + aws-cli - obtains a client token using the AWS CLI + Defaults to iam-authenticator + enum: + - iam-authenticator + - aws-cli + type: string + version: + description: |- + Version defines the desired Kubernetes version. If no version number + is supplied then the latest version of Kubernetes that EKS supports + will be used. + minLength: 2 + pattern: ^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.?(\.0|[1-9][0-9]*)?$ + type: string + vpcCni: + description: VpcCni is used to set configuration options for + the VPC CNI plugin + properties: + disable: + default: false + description: |- + Disable indicates that the Amazon VPC CNI should be disabled. With EKS clusters the + Amazon VPC CNI is automatically installed into the cluster. For clusters where you want + to use an alternate CNI this option provides a way to specify that the Amazon VPC CNI + should be deleted. You cannot set this to true if you are using the + Amazon VPC CNI addon. + type: boolean + env: + description: Env defines a list of environment variables + to apply to the `aws-node` DaemonSet + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + type: object + type: object + required: + - spec + type: object + required: + - template + type: object + type: object + served: true + storage: true diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclustertemplates.yaml new file mode 100644 index 0000000000..ea051696b0 --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclustertemplates.yaml @@ -0,0 +1,83 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: awsmanagedclustertemplates.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: AWSManagedClusterTemplate + listKind: AWSManagedClusterTemplateList + plural: awsmanagedclustertemplates + shortNames: + - amct + singular: awsmanagedclustertemplate + scope: Namespaced + versions: + - name: v1beta2 + schema: + openAPIV3Schema: + description: AWSManagedClusterTemplate is the Schema for the AWSManagedClusterTemplates + 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: AWSManagedClusterTemplateSpec defines the desired state of + AWSManagedClusterTemplate. + properties: + template: + description: AWSManagedClusterTemplateResource describes the data + needed to create an AWSManagedCluster from a template. + properties: + spec: + description: AWSManagedClusterSpec defines the desired state of + AWSManagedCluster + properties: + controlPlaneEndpoint: + description: ControlPlaneEndpoint represents the endpoint + used to communicate with the control plane. + properties: + host: + description: host is the hostname on which the API server + is serving. + maxLength: 512 + type: string + port: + description: port is the port on which the API server + is serving. + format: int32 + type: integer + required: + - host + - port + type: object + type: object + required: + - spec + type: object + required: + - template + type: object + type: object + served: true + storage: true diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b7fbdd0d73..c3f6177556 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -18,7 +18,9 @@ resources: - bases/infrastructure.cluster.x-k8s.io_awsclustercontrolleridentities.yaml - bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml - bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +- bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml - bases/infrastructure.cluster.x-k8s.io_awsmanagedclusters.yaml +- bases/infrastructure.cluster.x-k8s.io_awsmanagedclustertemplates.yaml - bases/bootstrap.cluster.x-k8s.io_eksconfigs.yaml - bases/bootstrap.cluster.x-k8s.io_eksconfigtemplates.yaml - bases/controlplane.cluster.x-k8s.io_rosacontrolplanes.yaml @@ -36,6 +38,7 @@ patchesStrategicMerge: - patches/webhook_in_awsclusterroleidentities.yaml - patches/webhook_in_awsclustertemplates.yaml - patches/webhook_in_awsmanagedcontrolplanes.yaml +- patches/webhook_in_awsmanagedcontrolplanetemplates.yaml - patches/webhook_in_eksconfigs.yaml - patches/webhook_in_eksconfigtemplates.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch @@ -49,7 +52,9 @@ patchesStrategicMerge: - patches/cainjection_in_awsclusterroleidentities.yaml - patches/cainjection_in_awsclustertemplates.yaml - patches/cainjection_in_awsmanagedcontrolplanes.yaml +- patches/cainjection_in_awsmanagedcontrolplanetemplates.yaml - patches/cainjection_in_awsmanagedclusters.yaml +- patches/cainjection_in_awsmanagedclustertemplates.yaml - patches/cainjection_in_eksconfigs.yaml - patches/cainjection_in_eksconfigtemplates.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch diff --git a/config/crd/patches/cainjection_in_awsmanagedclustertemplates.yaml b/config/crd/patches/cainjection_in_awsmanagedclustertemplates.yaml new file mode 100644 index 0000000000..8fc223d683 --- /dev/null +++ b/config/crd/patches/cainjection_in_awsmanagedclustertemplates.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: awsmanagedclustertemplates.infrastructure.cluster.x-k8s.io diff --git a/config/crd/patches/cainjection_in_awsmanagedcontrolplanetemplates.yaml b/config/crd/patches/cainjection_in_awsmanagedcontrolplanetemplates.yaml new file mode 100644 index 0000000000..9b53959b88 --- /dev/null +++ b/config/crd/patches/cainjection_in_awsmanagedcontrolplanetemplates.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io diff --git a/config/crd/patches/webhook_in_awsmanagedcontrolplanetemplates.yaml b/config/crd/patches/webhook_in_awsmanagedcontrolplanetemplates.yaml new file mode 100644 index 0000000000..fe20483dcb --- /dev/null +++ b/config/crd/patches/webhook_in_awsmanagedcontrolplanetemplates.yaml @@ -0,0 +1,16 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io +spec: + conversion: + strategy: Webhook + webhook: + conversionReviewVersions: ["v1", "v1beta1"] + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 5eebfae968..91dd9aa54b 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -289,6 +289,28 @@ webhooks: resources: - awsmanagedcontrolplanes sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-controlplane-cluster-x-k8s-io-v1beta2-awsmanagedcontrolplanetemplate + failurePolicy: Fail + matchPolicy: Equivalent + name: default.awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io + rules: + - apiGroups: + - controlplane.cluster.x-k8s.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - awsmanagedcontrolplanetemplates + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 @@ -625,6 +647,28 @@ webhooks: resources: - awsmanagedcontrolplanes sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-controlplane-cluster-x-k8s-io-v1beta2-awsmanagedcontrolplanetemplate + failurePolicy: Fail + matchPolicy: Equivalent + name: validation.awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io + rules: + - apiGroups: + - controlplane.cluster.x-k8s.io + apiVersions: + - v1beta2 + operations: + - CREATE + - UPDATE + resources: + - awsmanagedcontrolplanetemplates + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/controlplane/eks/api/v1beta1/conversion.go b/controlplane/eks/api/v1beta1/conversion.go index 9a0c2720c6..4039b113d4 100644 --- a/controlplane/eks/api/v1beta1/conversion.go +++ b/controlplane/eks/api/v1beta1/conversion.go @@ -29,17 +29,92 @@ import ( // ConvertTo converts the v1beta1 AWSManagedControlPlane receiver to a v1beta2 AWSManagedControlPlane. func (r *AWSManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*ekscontrolplanev1.AWSManagedControlPlane) - if err := Convert_v1beta1_AWSManagedControlPlane_To_v1beta2_AWSManagedControlPlane(r, dst, nil); err != nil { return err } // Manually restore data. restored := &ekscontrolplanev1.AWSManagedControlPlane{} - if ok, err := utilconversion.UnmarshalData(r, restored); err != nil || !ok { + if _, err := utilconversion.UnmarshalData(r, restored); err != nil { + return err + } + + dst.Spec.IdentityRef = r.Spec.IdentityRef + dst.Spec.NetworkSpec = r.Spec.NetworkSpec + dst.Spec.Region = r.Spec.Region + dst.Spec.SSHKeyName = r.Spec.SSHKeyName + dst.Spec.Version = r.Spec.Version + dst.Spec.RoleName = r.Spec.RoleName + dst.Spec.RoleAdditionalPolicies = r.Spec.RoleAdditionalPolicies + dst.Spec.AdditionalTags = r.Spec.AdditionalTags + + if r.Spec.Logging != nil { + dst.Spec.Logging = &ekscontrolplanev1.ControlPlaneLoggingSpec{} + if err := Convert_v1beta1_ControlPlaneLoggingSpec_To_v1beta2_ControlPlaneLoggingSpec(r.Spec.Logging, dst.Spec.Logging, nil); err != nil { + return err + } + } + + dst.Spec.SecondaryCidrBlock = r.Spec.SecondaryCidrBlock + + if err := Convert_v1beta1_EndpointAccess_To_v1beta2_EndpointAccess(&r.Spec.EndpointAccess, &dst.Spec.EndpointAccess, nil); err != nil { + return err + } + + if r.Spec.EncryptionConfig != nil { + dst.Spec.EncryptionConfig = &ekscontrolplanev1.EncryptionConfig{} + if err := Convert_v1beta1_EncryptionConfig_To_v1beta2_EncryptionConfig(r.Spec.EncryptionConfig, dst.Spec.EncryptionConfig, nil); err != nil { + return err + } + } + + dst.Spec.ImageLookupFormat = r.Spec.ImageLookupFormat + dst.Spec.ImageLookupOrg = r.Spec.ImageLookupOrg + dst.Spec.ImageLookupBaseOS = r.Spec.ImageLookupBaseOS + + if r.Spec.IAMAuthenticatorConfig != nil { + dst.Spec.IAMAuthenticatorConfig = &ekscontrolplanev1.IAMAuthenticatorConfig{} + if err := Convert_v1beta1_IAMAuthenticatorConfig_To_v1beta2_IAMAuthenticatorConfig(r.Spec.IAMAuthenticatorConfig, dst.Spec.IAMAuthenticatorConfig, nil); err != nil { + return err + } + } + + if r.Spec.Addons != nil { + addons := []ekscontrolplanev1.Addon{} + for _, addon := range *r.Spec.Addons { + var convertedAddon ekscontrolplanev1.Addon + if err := Convert_v1beta1_Addon_To_v1beta2_Addon(&addon, &convertedAddon, nil); err != nil { + return err + } + addons = append(addons, convertedAddon) + } + dst.Spec.Addons = &addons + } + + dst.Spec.Bastion = r.Spec.Bastion + + if r.Spec.TokenMethod != nil { + Convert_v1beta1_EKSTokenMethod_To_v1beta2_EKSTokenMethod(r.Spec.TokenMethod, &dst.Spec.TokenMethod) + } + + if err := Convert_v1beta1_VpcCni_To_v1beta2_VpcCni(&r.Spec.VpcCni, &dst.Spec.VpcCni, nil); err != nil { return err } dst.Spec.VpcCni.Disable = r.Spec.DisableVPCCNI + + if err := Convert_v1beta1_KubeProxy_To_v1beta2_KubeProxy(&r.Spec.KubeProxy, &dst.Spec.KubeProxy, nil); err != nil { + return err + } + + dst.Spec.AssociateOIDCProvider = r.Spec.AssociateOIDCProvider + + if r.Spec.OIDCIdentityProviderConfig != nil { + dst.Spec.OIDCIdentityProviderConfig = &ekscontrolplanev1.OIDCIdentityProviderConfig{} + if err := Convert_v1beta1_OIDCIdentityProviderConfig_To_v1beta2_OIDCIdentityProviderConfig(r.Spec.OIDCIdentityProviderConfig, dst.Spec.OIDCIdentityProviderConfig, nil); err != nil { + return err + } + } + dst.Spec.Partition = restored.Spec.Partition dst.Spec.RestrictPrivateSubnets = restored.Spec.RestrictPrivateSubnets dst.Spec.RolePath = restored.Spec.RolePath @@ -52,12 +127,86 @@ func (r *AWSManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error { // ConvertFrom converts the v1beta2 AWSManagedControlPlane receiver to a v1beta1 AWSManagedControlPlane. func (r *AWSManagedControlPlane) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*ekscontrolplanev1.AWSManagedControlPlane) - if err := Convert_v1beta2_AWSManagedControlPlane_To_v1beta1_AWSManagedControlPlane(src, r, nil); err != nil { return err } + r.Spec.IdentityRef = src.Spec.IdentityRef + r.Spec.NetworkSpec = src.Spec.NetworkSpec + r.Spec.Region = src.Spec.Region + r.Spec.SSHKeyName = src.Spec.SSHKeyName + r.Spec.Version = src.Spec.Version + r.Spec.RoleName = src.Spec.RoleName + r.Spec.RoleAdditionalPolicies = src.Spec.RoleAdditionalPolicies + r.Spec.AdditionalTags = src.Spec.AdditionalTags + + if src.Spec.Logging != nil { + r.Spec.Logging = &ControlPlaneLoggingSpec{} + if err := Convert_v1beta2_ControlPlaneLoggingSpec_To_v1beta1_ControlPlaneLoggingSpec(src.Spec.Logging, r.Spec.Logging, nil); err != nil { + return err + } + } + + r.Spec.SecondaryCidrBlock = src.Spec.SecondaryCidrBlock + + if err := Convert_v1beta2_EndpointAccess_To_v1beta1_EndpointAccess(&src.Spec.EndpointAccess, &r.Spec.EndpointAccess, nil); err != nil { + return err + } + + if src.Spec.EncryptionConfig != nil { + r.Spec.EncryptionConfig = &EncryptionConfig{} + if err := Convert_v1beta2_EncryptionConfig_To_v1beta1_EncryptionConfig(src.Spec.EncryptionConfig, r.Spec.EncryptionConfig, nil); err != nil { + return err + } + } + + r.Spec.ImageLookupFormat = src.Spec.ImageLookupFormat + r.Spec.ImageLookupOrg = src.Spec.ImageLookupOrg + r.Spec.ImageLookupBaseOS = src.Spec.ImageLookupBaseOS + + if src.Spec.IAMAuthenticatorConfig != nil { + r.Spec.IAMAuthenticatorConfig = &IAMAuthenticatorConfig{} + if err := Convert_v1beta2_IAMAuthenticatorConfig_To_v1beta1_IAMAuthenticatorConfig(src.Spec.IAMAuthenticatorConfig, r.Spec.IAMAuthenticatorConfig, nil); err != nil { + return err + } + } + + if src.Spec.Addons != nil { + addons := []Addon{} + for _, addon := range *src.Spec.Addons { + var convertedAddon Addon + if err := Convert_v1beta2_Addon_To_v1beta1_Addon(&addon, &convertedAddon, nil); err != nil { + return err + } + addons = append(addons, convertedAddon) + } + r.Spec.Addons = &addons + } + + r.Spec.Bastion = src.Spec.Bastion + + if src.Spec.TokenMethod != nil { + Convert_v1beta2_EKSTokenMethod_To_v1beta1_EKSTokenMethod(src.Spec.TokenMethod, &r.Spec.TokenMethod) + } + + if err := Convert_v1beta2_VpcCni_To_v1beta1_VpcCni(&src.Spec.VpcCni, &r.Spec.VpcCni, nil); err != nil { + return err + } r.Spec.DisableVPCCNI = src.Spec.VpcCni.Disable + + if err := Convert_v1beta2_KubeProxy_To_v1beta1_KubeProxy(&src.Spec.KubeProxy, &r.Spec.KubeProxy, nil); err != nil { + return err + } + + r.Spec.AssociateOIDCProvider = src.Spec.AssociateOIDCProvider + + if src.Spec.OIDCIdentityProviderConfig != nil { + r.Spec.OIDCIdentityProviderConfig = &OIDCIdentityProviderConfig{} + if err := Convert_v1beta2_OIDCIdentityProviderConfig_To_v1beta1_OIDCIdentityProviderConfig(src.Spec.OIDCIdentityProviderConfig, r.Spec.OIDCIdentityProviderConfig, nil); err != nil { + return err + } + } + if err := utilconversion.MarshalData(src, r); err != nil { return err } @@ -126,3 +275,21 @@ func Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlan func Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in *ekscontrolplanev1.AWSManagedControlPlaneStatus, out *AWSManagedControlPlaneStatus, s apiconversion.Scope) error { return autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in, out, s) } + +func Convert_v1beta1_EKSTokenMethod_To_v1beta2_EKSTokenMethod(src *EKSTokenMethod, dst **ekscontrolplanev1.EKSTokenMethod) { + if src == nil { + *dst = nil + return + } + tokenMethod := ekscontrolplanev1.EKSTokenMethod(*src) + *dst = &tokenMethod +} + +func Convert_v1beta2_EKSTokenMethod_To_v1beta1_EKSTokenMethod(src *ekscontrolplanev1.EKSTokenMethod, dst **EKSTokenMethod) { + if src == nil { + *dst = nil + return + } + tokenMethod := EKSTokenMethod(*src) + *dst = &tokenMethod +} diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go index 8970b29cd7..ffb4e891b8 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go @@ -223,69 +223,83 @@ func (r *AWSManagedControlPlane) validateEKSClusterNameSame(old *AWSManagedContr func (r *AWSManagedControlPlane) validateEKSVersion(old *AWSManagedControlPlane) field.ErrorList { path := field.NewPath("spec.version") + var oldVersion *string + if old != nil { + oldVersion = old.Spec.Version + } + return validateEKSVersion(r.Spec.Version, oldVersion, r.Spec.NetworkSpec, path) +} + +func validateEKSVersion(eksVersion *string, oldVersion *string, networkSpec infrav1.NetworkSpec, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.Version == nil { + if eksVersion == nil { return allErrs } - v, err := parseEKSVersion(*r.Spec.Version) + v, err := parseEKSVersion(*eksVersion) if err != nil { - allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, err.Error())) + allErrs = append(allErrs, field.Invalid(path, *eksVersion, err.Error())) } - if old != nil && old.Spec.Version != nil { - oldV, err := parseEKSVersion(*old.Spec.Version) + if oldVersion != nil { + oldV, err := parseEKSVersion(*oldVersion) if err == nil && (v.Major() < oldV.Major() || v.Minor() < oldV.Minor()) { - allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, "new version less than old version")) + allErrs = append(allErrs, field.Invalid(path, *eksVersion, "new version less than old version")) } } - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() { + if networkSpec.VPC.IsIPv6Enabled() { minIPv6, _ := version.ParseSemantic(minKubeVersionForIPv6) if v.LessThan(minIPv6) { - allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, fmt.Sprintf("IPv6 requires Kubernetes %s or greater", minKubeVersionForIPv6))) + allErrs = append(allErrs, field.Invalid(path, *eksVersion, fmt.Sprintf("IPv6 requires Kubernetes %s or greater", minKubeVersionForIPv6))) } } return allErrs } func (r *AWSManagedControlPlane) validateEKSAddons() field.ErrorList { + return validateEKSAddons(r.Spec.Version, r.Spec.NetworkSpec, r.Spec.Addons, field.NewPath("spec")) +} + +func validateEKSAddons(eksVersion *string, networkSpec infrav1.NetworkSpec, addons *[]Addon, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if !r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && (r.Spec.Addons == nil || len(*r.Spec.Addons) == 0) { + // If not using IPv6 and no addons are specified, return no errors + if !networkSpec.VPC.IsIPv6Enabled() && (addons == nil || len(*addons) == 0) { return allErrs } - if r.Spec.Version == nil { + // Version is required for addon validation + if eksVersion == nil { return allErrs } - path := field.NewPath("spec.version") - v, err := parseEKSVersion(*r.Spec.Version) + versionPath := path.Child("version") + v, err := parseEKSVersion(*eksVersion) if err != nil { - allErrs = append(allErrs, field.Invalid(path, *r.Spec.Version, err.Error())) + allErrs = append(allErrs, field.Invalid(versionPath, *eksVersion, err.Error())) } minVersion, _ := version.ParseSemantic(minAddonVersion) - addonsPath := field.NewPath("spec.addons") + addonsPath := path.Child("addons") if v.LessThan(minVersion) { - message := fmt.Sprintf("addons requires Kubernetes %s or greater", minAddonVersion) - allErrs = append(allErrs, field.Invalid(addonsPath, *r.Spec.Version, message)) + message := fmt.Sprintf("addons require Kubernetes %s or greater", minAddonVersion) + allErrs = append(allErrs, field.Invalid(addonsPath, *eksVersion, message)) } // validations for IPv6: // - addons have to be defined in case IPv6 is enabled // - minimum version requirement for VPC-CNI using IPv6 ipFamily is 1.10.2 - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() { - if r.Spec.Addons == nil || len(*r.Spec.Addons) == 0 { + if networkSpec.VPC.IsIPv6Enabled() { + if addons == nil || len(*addons) == 0 { allErrs = append(allErrs, field.Invalid(addonsPath, "", "addons are required to be set explicitly if IPv6 is enabled")) return allErrs } - for _, addon := range *r.Spec.Addons { + for _, addon := range *addons { if addon.Name == vpcCniAddon { v, err := version.ParseGeneric(addon.Version) if err != nil { @@ -305,18 +319,18 @@ func (r *AWSManagedControlPlane) validateEKSAddons() field.ErrorList { } func (r *AWSManagedControlPlane) validateIAMAuthConfig() field.ErrorList { - var allErrs field.ErrorList + return validateIAMAuthConfig(r.Spec.IAMAuthenticatorConfig, field.NewPath("spec.iamAuthenticatorConfig")) +} - parentPath := field.NewPath("spec.iamAuthenticatorConfig") +func validateIAMAuthConfig(cfg *IAMAuthenticatorConfig, parentPath *field.Path) field.ErrorList { + var allErrs field.ErrorList - cfg := r.Spec.IAMAuthenticatorConfig if cfg == nil { return allErrs } for i, userMapping := range cfg.UserMappings { - usersPathName := fmt.Sprintf("mapUsers[%d]", i) - usersPath := parentPath.Child(usersPathName) + usersPath := parentPath.Child(fmt.Sprintf("mapUsers[%d]", i)) errs := userMapping.Validate() for _, validErr := range errs { allErrs = append(allErrs, field.Invalid(usersPath, userMapping, validErr.Error())) @@ -324,8 +338,7 @@ func (r *AWSManagedControlPlane) validateIAMAuthConfig() field.ErrorList { } for i, roleMapping := range cfg.RoleMappings { - rolePathName := fmt.Sprintf("mapRoles[%d]", i) - rolePath := parentPath.Child(rolePathName) + rolePath := parentPath.Child(fmt.Sprintf("mapRoles[%d]", i)) errs := roleMapping.Validate() for _, validErr := range errs { allErrs = append(allErrs, field.Invalid(rolePath, roleMapping, validErr.Error())) @@ -336,157 +349,192 @@ func (r *AWSManagedControlPlane) validateIAMAuthConfig() field.ErrorList { } func (r *AWSManagedControlPlane) validateSecondaryCIDR() field.ErrorList { + return validateSecondaryCIDR(r.Spec.SecondaryCidrBlock, field.NewPath("spec", "secondaryCidrBlock")) +} + +func validateSecondaryCIDR(secondaryCidrBlock *string, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.SecondaryCidrBlock != nil { - cidrField := field.NewPath("spec", "secondaryCidrBlock") + if secondaryCidrBlock != nil { _, validRange1, _ := net.ParseCIDR("100.64.0.0/10") _, validRange2, _ := net.ParseCIDR("198.19.0.0/16") - _, ipv4Net, err := net.ParseCIDR(*r.Spec.SecondaryCidrBlock) + _, ipv4Net, err := net.ParseCIDR(*secondaryCidrBlock) if err != nil { - allErrs = append(allErrs, field.Invalid(cidrField, *r.Spec.SecondaryCidrBlock, "must be valid CIDR range")) + allErrs = append(allErrs, field.Invalid(path, *secondaryCidrBlock, "must be a valid CIDR range")) return allErrs } cidrSize := cidr.AddressCount(ipv4Net) if cidrSize > cidrSizeMax || cidrSize < cidrSizeMin { - allErrs = append(allErrs, field.Invalid(cidrField, *r.Spec.SecondaryCidrBlock, "CIDR block sizes must be between a /16 netmask and /28 netmask")) + allErrs = append(allErrs, field.Invalid(path, *secondaryCidrBlock, "CIDR block sizes must be between a /16 netmask and /28 netmask")) } start, end := cidr.AddressRange(ipv4Net) if (!validRange1.Contains(start) || !validRange1.Contains(end)) && (!validRange2.Contains(start) || !validRange2.Contains(end)) { - allErrs = append(allErrs, field.Invalid(cidrField, *r.Spec.SecondaryCidrBlock, "must be within the 100.64.0.0/10 or 198.19.0.0/16 range")) + allErrs = append(allErrs, field.Invalid(path, *secondaryCidrBlock, "must be within the 100.64.0.0/10 or 198.19.0.0/16 range")) } } - - if len(allErrs) == 0 { - return nil - } return allErrs } func (r *AWSManagedControlPlane) validateKubeProxy() field.ErrorList { + return validateKubeProxy(r.Spec.KubeProxy, r.Spec.Addons, field.NewPath("spec")) +} + +func validateKubeProxy(kubeProxy KubeProxy, addons *[]Addon, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.KubeProxy.Disable { - disableField := field.NewPath("spec", "kubeProxy", "disable") + if kubeProxy.Disable { + disableField := path.Child("kubeProxy", "disable") - if r.Spec.Addons != nil { - for _, addon := range *r.Spec.Addons { + if addons != nil { + for _, addon := range *addons { if addon.Name == kubeProxyAddon { - allErrs = append(allErrs, field.Invalid(disableField, r.Spec.KubeProxy.Disable, "cannot disable kube-proxy if the kube-proxy addon is specified")) + allErrs = append(allErrs, field.Invalid(disableField, kubeProxy.Disable, "cannot disable kube-proxy if the kube-proxy addon is specified")) break } } } } - if len(allErrs) == 0 { - return nil - } return allErrs } func (r *AWSManagedControlPlane) validateDisableVPCCNI() field.ErrorList { + return validateDisableVPCCNI(r.Spec.VpcCni, r.Spec.Addons, field.NewPath("spec")) +} + +func validateDisableVPCCNI(vpcCni VpcCni, addons *[]Addon, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.VpcCni.Disable { - disableField := field.NewPath("spec", "vpcCni", "disable") + if vpcCni.Disable { + disableField := path.Child("vpcCni", "disable") - if r.Spec.Addons != nil { - for _, addon := range *r.Spec.Addons { + if addons != nil { + for _, addon := range *addons { if addon.Name == vpcCniAddon { - allErrs = append(allErrs, field.Invalid(disableField, r.Spec.VpcCni.Disable, "cannot disable vpc cni if the vpc-cni addon is specified")) + allErrs = append(allErrs, field.Invalid(disableField, vpcCni.Disable, "cannot disable vpc cni if the vpc-cni addon is specified")) break } } } } - if len(allErrs) == 0 { - return nil - } return allErrs } func (r *AWSManagedControlPlane) validateRestrictPrivateSubnets() field.ErrorList { + return validateRestrictPrivateSubnets(r.Spec.RestrictPrivateSubnets, r.Spec.NetworkSpec, r.Spec.EKSClusterName, field.NewPath("spec")) +} + +func validateRestrictPrivateSubnets(restrictPrivateSubnets bool, networkSpec infrav1.NetworkSpec, eksClusterName string, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.RestrictPrivateSubnets && r.Spec.NetworkSpec.VPC.IsUnmanaged(r.Spec.EKSClusterName) { - boolField := field.NewPath("spec", "restrictPrivateSubnets") - if len(r.Spec.NetworkSpec.Subnets.FilterPrivate()) == 0 { - allErrs = append(allErrs, field.Invalid(boolField, r.Spec.RestrictPrivateSubnets, "cannot enable private subnets restriction when no private subnets are specified")) + if restrictPrivateSubnets && networkSpec.VPC.IsUnmanaged(eksClusterName) { + boolField := path.Child("restrictPrivateSubnets") + if len(networkSpec.Subnets.FilterPrivate()) == 0 { + allErrs = append(allErrs, field.Invalid(boolField, restrictPrivateSubnets, "cannot enable private subnets restriction when no private subnets are specified")) } } - if len(allErrs) == 0 { - return nil - } return allErrs } func (r *AWSManagedControlPlane) validatePrivateDNSHostnameTypeOnLaunch() field.ErrorList { + return validatePrivateDNSHostnameTypeOnLaunch(r.Spec.NetworkSpec, field.NewPath("spec")) +} + +func validatePrivateDNSHostnameTypeOnLaunch(networkSpec infrav1.NetworkSpec, path *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch != nil && *r.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch != hostnameTypeResourceName { - privateDNSHostnameTypeOnLaunch := field.NewPath("spec", "networkSpec", "vpc", "privateDNSHostnameTypeOnLaunch") - allErrs = append(allErrs, field.Invalid(privateDNSHostnameTypeOnLaunch, r.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch, fmt.Sprintf("only %s HostnameType can be used in IPv6 mode", hostnameTypeResourceName))) + if networkSpec.VPC.IsIPv6Enabled() && networkSpec.VPC.PrivateDNSHostnameTypeOnLaunch != nil && *networkSpec.VPC.PrivateDNSHostnameTypeOnLaunch != hostnameTypeResourceName { + privateDNSHostnameTypeOnLaunchPath := path.Child("networkSpec", "vpc", "privateDNSHostnameTypeOnLaunch") + allErrs = append(allErrs, field.Invalid( + privateDNSHostnameTypeOnLaunchPath, networkSpec.VPC.PrivateDNSHostnameTypeOnLaunch, + fmt.Sprintf("only %s HostnameType can be used in IPv6 mode", hostnameTypeResourceName), + )) } return allErrs } func (r *AWSManagedControlPlane) validateNetwork() field.ErrorList { + return validateNetwork("AWSManagedControlPlane", r.Spec.NetworkSpec, r.Spec.SecondaryCidrBlock, field.NewPath("spec")) +} + +func validateNetwork(resourceName string, networkSpec infrav1.NetworkSpec, secondaryCidrBlock *string, path *field.Path) field.ErrorList { var allErrs field.ErrorList // If only `AWSManagedControlPlane.spec.secondaryCidrBlock` is set, no additional checks are done to remain // backward-compatible. The `VPCSpec.SecondaryCidrBlocks` field was added later - if that list is not empty, we // require `AWSManagedControlPlane.spec.secondaryCidrBlock` to be listed in there as well. This may allow merging // the fields later on. - podSecondaryCidrBlock := r.Spec.SecondaryCidrBlock - secondaryCidrBlocks := r.Spec.NetworkSpec.VPC.SecondaryCidrBlocks - secondaryCidrBlocksField := field.NewPath("spec", "network", "vpc", "secondaryCidrBlocks") - if podSecondaryCidrBlock != nil && len(secondaryCidrBlocks) > 0 { + secondaryCidrBlocks := networkSpec.VPC.SecondaryCidrBlocks + secondaryCidrBlocksField := path.Child("network", "vpc", "secondaryCidrBlocks") + if secondaryCidrBlock != nil && len(secondaryCidrBlocks) > 0 { found := false for _, cidrBlock := range secondaryCidrBlocks { - if cidrBlock.IPv4CidrBlock == *podSecondaryCidrBlock { + if cidrBlock.IPv4CidrBlock == *secondaryCidrBlock { found = true break } } if !found { - allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField, secondaryCidrBlocks, fmt.Sprintf("AWSManagedControlPlane.spec.secondaryCidrBlock %v must be listed in AWSManagedControlPlane.spec.network.vpc.secondaryCidrBlocks (required if both fields are filled)", *podSecondaryCidrBlock))) + allErrs = append(allErrs, field.Invalid( + secondaryCidrBlocksField, secondaryCidrBlocks, + fmt.Sprintf("%s.spec.secondaryCidrBlock %v must be listed in %s.spec.network.vpc.secondaryCidrBlocks (required if both fields are filled)", resourceName, *secondaryCidrBlock, resourceName), + )) } } - if podSecondaryCidrBlock != nil && r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.CidrBlock == *podSecondaryCidrBlock { - secondaryCidrBlockField := field.NewPath("spec", "vpc", "secondaryCidrBlock") - allErrs = append(allErrs, field.Invalid(secondaryCidrBlockField, secondaryCidrBlocks, fmt.Sprintf("AWSManagedControlPlane.spec.secondaryCidrBlock %v must not be equal to the primary AWSManagedControlPlane.spec.network.vpc.cidrBlock", *podSecondaryCidrBlock))) + if secondaryCidrBlock != nil && networkSpec.VPC.CidrBlock != "" && networkSpec.VPC.CidrBlock == *secondaryCidrBlock { + secondaryCidrBlockField := path.Child("vpc", "secondaryCidrBlock") + allErrs = append(allErrs, field.Invalid( + secondaryCidrBlockField, secondaryCidrBlocks, + fmt.Sprintf("%s.spec.secondaryCidrBlock %v must not be equal to the primary %s.spec.network.vpc.cidrBlock", resourceName, *secondaryCidrBlock, resourceName), + )) } + for _, cidrBlock := range secondaryCidrBlocks { - if r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.CidrBlock == cidrBlock.IPv4CidrBlock { - allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField, secondaryCidrBlocks, fmt.Sprintf("AWSManagedControlPlane.spec.network.vpc.secondaryCidrBlocks must not contain the primary AWSManagedControlPlane.spec.network.vpc.cidrBlock %v", r.Spec.NetworkSpec.VPC.CidrBlock))) + if networkSpec.VPC.CidrBlock != "" && networkSpec.VPC.CidrBlock == cidrBlock.IPv4CidrBlock { + allErrs = append(allErrs, field.Invalid( + secondaryCidrBlocksField, secondaryCidrBlocks, + fmt.Sprintf("%s.spec.network.vpc.secondaryCidrBlocks must not contain the primary %s.spec.network.vpc.cidrBlock %v", resourceName, resourceName, networkSpec.VPC.CidrBlock), + )) } } - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.CidrBlock != "" && r.Spec.NetworkSpec.VPC.IPv6.PoolID == "" { - poolField := field.NewPath("spec", "network", "vpc", "ipv6", "poolId") - allErrs = append(allErrs, field.Invalid(poolField, r.Spec.NetworkSpec.VPC.IPv6.PoolID, "poolId cannot be empty if cidrBlock is set")) - } + // IPv6 validations + if networkSpec.VPC.IsIPv6Enabled() { + ipv6Path := path.Child("network", "vpc", "ipv6") - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.PoolID != "" && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool != nil { - poolField := field.NewPath("spec", "network", "vpc", "ipv6", "poolId") - allErrs = append(allErrs, field.Invalid(poolField, r.Spec.NetworkSpec.VPC.IPv6.PoolID, "poolId and ipamPool cannot be used together")) - } + if networkSpec.VPC.IPv6.CidrBlock != "" && networkSpec.VPC.IPv6.PoolID == "" { + allErrs = append(allErrs, field.Invalid( + ipv6Path.Child("poolId"), networkSpec.VPC.IPv6.PoolID, + "poolId cannot be empty if cidrBlock is set", + )) + } - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.CidrBlock != "" && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool != nil { - cidrBlockField := field.NewPath("spec", "network", "vpc", "ipv6", "cidrBlock") - allErrs = append(allErrs, field.Invalid(cidrBlockField, r.Spec.NetworkSpec.VPC.IPv6.CidrBlock, "cidrBlock and ipamPool cannot be used together")) - } + if networkSpec.VPC.IPv6.PoolID != "" && networkSpec.VPC.IPv6.IPAMPool != nil { + allErrs = append(allErrs, field.Invalid( + ipv6Path.Child("poolId"), networkSpec.VPC.IPv6.PoolID, + "poolId and ipamPool cannot be used together", + )) + } + + if networkSpec.VPC.IPv6.CidrBlock != "" && networkSpec.VPC.IPv6.IPAMPool != nil { + allErrs = append(allErrs, field.Invalid( + ipv6Path.Child("cidrBlock"), networkSpec.VPC.IPv6.CidrBlock, + "cidrBlock and ipamPool cannot be used together", + )) + } - if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool != nil && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool.ID == "" && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool.Name == "" { - ipamPoolField := field.NewPath("spec", "network", "vpc", "ipv6", "ipamPool") - allErrs = append(allErrs, field.Invalid(ipamPoolField, r.Spec.NetworkSpec.VPC.IPv6.IPAMPool, "ipamPool must have either id or name")) + if networkSpec.VPC.IPv6.IPAMPool != nil && networkSpec.VPC.IPv6.IPAMPool.ID == "" && networkSpec.VPC.IPv6.IPAMPool.Name == "" { + allErrs = append(allErrs, field.Invalid( + ipv6Path.Child("ipamPool"), networkSpec.VPC.IPv6.IPAMPool, + "ipamPool must have either id or name", + )) + } } return allErrs diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go index 16727b1c82..4a6b79a782 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go @@ -85,52 +85,118 @@ func TestDefaultingWebhook(t *testing.T) { resourceName: "cluster1", resourceNS: "default", expectHash: false, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + IdentityRef: defaultIdentityRef, + Bastion: defaultTestBastion, + NetworkSpec: defaultNetworkSpec, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, { name: "less than 100 chars, dot in name", resourceName: "team1.cluster1", resourceNS: "default", expectHash: false, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_team1_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_team1_cluster1", + IdentityRef: defaultIdentityRef, + Bastion: defaultTestBastion, + NetworkSpec: defaultNetworkSpec, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, { name: "more than 100 chars", resourceName: "abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde", resourceNS: "default", expectHash: true, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "capi_", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "capi_", + IdentityRef: defaultIdentityRef, + Bastion: defaultTestBastion, + NetworkSpec: defaultNetworkSpec, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, { name: "with patch", resourceName: "cluster1", resourceNS: "default", expectHash: false, - spec: AWSManagedControlPlaneSpec{Version: &vV1_17_1}, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", Version: &vV1_17_1, IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + spec: AWSManagedControlPlaneSpec{ + Version: &vV1_17_1, + }, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + Version: &vV1_17_1, + IdentityRef: defaultIdentityRef, + Bastion: defaultTestBastion, + NetworkSpec: defaultNetworkSpec, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, { name: "with allowed ip on bastion", resourceName: "cluster1", resourceNS: "default", expectHash: false, - spec: AWSManagedControlPlaneSpec{Bastion: infrav1.Bastion{AllowedCIDRBlocks: []string{"100.100.100.100/0"}}}, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: infrav1.Bastion{AllowedCIDRBlocks: []string{"100.100.100.100/0"}}, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + spec: AWSManagedControlPlaneSpec{ + Bastion: infrav1.Bastion{ + AllowedCIDRBlocks: []string{"100.100.100.100/0"}, + }, + }, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + IdentityRef: defaultIdentityRef, + Bastion: infrav1.Bastion{ + AllowedCIDRBlocks: []string{"100.100.100.100/0"}, + }, + NetworkSpec: defaultNetworkSpec, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, { name: "with CNI on network", resourceName: "cluster1", resourceNS: "default", expectHash: false, - spec: AWSManagedControlPlaneSpec{NetworkSpec: infrav1.NetworkSpec{CNI: &infrav1.CNISpec{}}}, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: infrav1.NetworkSpec{CNI: &infrav1.CNISpec{}, VPC: defaultVPCSpec}, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + spec: AWSManagedControlPlaneSpec{ + NetworkSpec: infrav1.NetworkSpec{ + CNI: &infrav1.CNISpec{}, + }, + }, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + IdentityRef: defaultIdentityRef, + Bastion: defaultTestBastion, + NetworkSpec: infrav1.NetworkSpec{ + CNI: &infrav1.CNISpec{}, + VPC: defaultVPCSpec, + }, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, { name: "secondary CIDR", resourceName: "cluster1", resourceNS: "default", expectHash: false, - expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, SecondaryCidrBlock: nil, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, + expectSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + IdentityRef: defaultIdentityRef, + Bastion: defaultTestBastion, + NetworkSpec: defaultNetworkSpec, + SecondaryCidrBlock: nil, + TokenMethod: &EKSTokenMethodIAMAuthenticator, + BootstrapSelfManagedAddons: true, + }, }, } diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplanetemplate_types.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplanetemplate_types.go new file mode 100644 index 0000000000..3c1b4109b8 --- /dev/null +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplanetemplate_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" +) + +// AWSManagedControlPlaneTemplateSpec defines the desired state of AWSManagedControlPlaneTemplate. +type AWSManagedControlPlaneTemplateSpec struct { + Template AWSManagedControlPlaneTemplateResource `json:"template"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=awsmanagedcontrolplanetemplates,scope=Namespaced,categories=cluster-api,shortName=awmcpt +// +kubebuilder:storageversion + +// AWSManagedControlPlaneTemplate is the Schema for the AWSManagedControlPlaneTemplates API. +type AWSManagedControlPlaneTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AWSManagedControlPlaneTemplateSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// AWSManagedControlPlaneTemplateList contains a list of AWSManagedControlPlaneTemplates. +type AWSManagedControlPlaneTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AWSManagedControlPlaneTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AWSManagedControlPlaneTemplate{}, &AWSManagedControlPlaneTemplateList{}) +} + +// AWSManagedControlPlaneTemplateResource describes the data needed to create an AWSManagedCluster from a template. +type AWSManagedControlPlaneTemplateResource struct { + Spec AWSManagedControlPlaneSpec `json:"spec"` +} diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplanetemplate_webhook.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplanetemplate_webhook.go new file mode 100644 index 0000000000..8dde7af2c6 --- /dev/null +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplanetemplate_webhook.go @@ -0,0 +1,241 @@ +/* +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 ( + "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" + + infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" +) + +// templateLog is used for logging in this package. +var templateLog = ctrl.Log.WithName("awsmanagedcontrolplanetemplate-resource") + +// SetupWebhookWithManager sets up the webhook with the Manager. +func (r *AWSManagedControlPlaneTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { + w := new(awsManagedControlPlaneTemplateWebhook) + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithValidator(w). + WithDefaulter(w). + Complete() +} + +// +kubebuilder:webhook:verbs=create;update,path=/validate-controlplane-cluster-x-k8s-io-v1beta2-awsmanagedcontrolplanetemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanetemplates,versions=v1beta2,name=validation.awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 +// +kubebuilder:webhook:verbs=create;update,path=/mutate-controlplane-cluster-x-k8s-io-v1beta2-awsmanagedcontrolplanetemplate,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=controlplane.cluster.x-k8s.io,resources=awsmanagedcontrolplanetemplates,versions=v1beta2,name=default.awsmanagedcontrolplanetemplates.controlplane.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 + +type awsManagedControlPlaneTemplateWebhook struct{} + +var _ webhook.CustomDefaulter = &awsManagedControlPlaneTemplateWebhook{} +var _ webhook.CustomValidator = &awsManagedControlPlaneTemplateWebhook{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type. +func (*awsManagedControlPlaneTemplateWebhook) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*AWSManagedControlPlaneTemplate) + if !ok { + return nil, fmt.Errorf("expected an AWSManagedControlPlaneTemplate object but got %T", r) + } + + templateLog.Info("Validating AWSManagedControlPlaneTemplate create", "name", r.Name) + + var allErrs field.ErrorList + + allErrs = append(allErrs, r.validateEKSVersion(nil)...) + allErrs = append(allErrs, r.Spec.Template.Spec.Bastion.Validate()...) + allErrs = append(allErrs, r.validateIAMAuthConfig()...) + allErrs = append(allErrs, r.validateSecondaryCIDR()...) + allErrs = append(allErrs, r.validateEKSAddons()...) + allErrs = append(allErrs, r.validateDisableVPCCNI()...) + allErrs = append(allErrs, r.validateRestrictPrivateSubnets()...) + allErrs = append(allErrs, r.validateKubeProxy()...) + allErrs = append(allErrs, r.Spec.Template.Spec.AdditionalTags.Validate()...) + allErrs = append(allErrs, r.validateNetwork()...) + allErrs = append(allErrs, r.validatePrivateDNSHostnameTypeOnLaunch()...) + + if len(allErrs) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. +func (*awsManagedControlPlaneTemplateWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + r, ok := newObj.(*AWSManagedControlPlaneTemplate) + if !ok { + return nil, fmt.Errorf("expected an AWSManagedControlPlaneTemplate object but got %T", r) + } + + templateLog.Info("AWSManagedControlPlaneTemplate validate update", "control-plane-template", klog.KObj(r)) + + oldAWSManagedControlplaneTemplate, ok := oldObj.(*AWSManagedControlPlaneTemplate) + if !ok { + return nil, apierrors.NewInvalid( + GroupVersion.WithKind("AWSManagedControlPlaneTemplate").GroupKind(), + r.Name, + field.ErrorList{field.InternalError(nil, errors.New("failed to convert old AWSManagedControlPlaneTemplate to object"))}, + ) + } + + var allErrs field.ErrorList + allErrs = append(allErrs, r.validateEKSVersion(oldAWSManagedControlplaneTemplate)...) + allErrs = append(allErrs, r.Spec.Template.Spec.Bastion.Validate()...) + allErrs = append(allErrs, r.validateIAMAuthConfig()...) + allErrs = append(allErrs, r.validateSecondaryCIDR()...) + allErrs = append(allErrs, r.validateEKSAddons()...) + allErrs = append(allErrs, r.validateDisableVPCCNI()...) + allErrs = append(allErrs, r.validateRestrictPrivateSubnets()...) + allErrs = append(allErrs, r.validateKubeProxy()...) + allErrs = append(allErrs, r.Spec.Template.Spec.AdditionalTags.Validate()...) + allErrs = append(allErrs, r.validatePrivateDNSHostnameTypeOnLaunch()...) + + if r.Spec.Template.Spec.Region != oldAWSManagedControlplaneTemplate.Spec.Template.Spec.Region { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "region"), r.Spec.Template.Spec.Region, "field is immutable"), + ) + } + + // If encryptionConfig is already set, do not allow removal of it. + if oldAWSManagedControlplaneTemplate.Spec.Template.Spec.EncryptionConfig != nil && r.Spec.Template.Spec.EncryptionConfig == nil { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "encryptionConfig"), r.Spec.Template.Spec.EncryptionConfig, "disabling EKS encryption is not allowed after it has been enabled"), + ) + } + + // If encryptionConfig is already set, do not allow change in provider + if r.Spec.Template.Spec.EncryptionConfig != nil && + r.Spec.Template.Spec.EncryptionConfig.Provider != nil && + oldAWSManagedControlplaneTemplate.Spec.Template.Spec.EncryptionConfig != nil && + oldAWSManagedControlplaneTemplate.Spec.Template.Spec.EncryptionConfig.Provider != nil && + *r.Spec.Template.Spec.EncryptionConfig.Provider != *oldAWSManagedControlplaneTemplate.Spec.Template.Spec.EncryptionConfig.Provider { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "encryptionConfig", "provider"), r.Spec.Template.Spec.EncryptionConfig.Provider, "changing EKS encryption is not allowed after it has been enabled"), + ) + } + + // If a identityRef is already set, do not allow removal of it. + if oldAWSManagedControlplaneTemplate.Spec.Template.Spec.IdentityRef != nil && r.Spec.Template.Spec.IdentityRef == nil { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "identityRef"), + r.Spec.Template.Spec.IdentityRef, "field cannot be set to nil"), + ) + } + + if oldAWSManagedControlplaneTemplate.Spec.Template.Spec.NetworkSpec.VPC.IsIPv6Enabled() != r.Spec.Template.Spec.NetworkSpec.VPC.IsIPv6Enabled() { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "network", "vpc", "enableIPv6"), r.Spec.Template.Spec.NetworkSpec.VPC.IsIPv6Enabled(), "changing IP family is not allowed after it has been set")) + } + + if len(allErrs) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. +func (*awsManagedControlPlaneTemplateWebhook) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*AWSManagedControlPlaneTemplate) + if !ok { + return nil, fmt.Errorf("expected an AWSManagedControlPlaneTemplate object but got %T", r) + } + + templateLog.Info("Validating AWSManagedControlPlaneTemplate delete", "name", r.Name) + // No validation logic on deletion. + return nil, nil +} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the type. +func (*awsManagedControlPlaneTemplateWebhook) Default(_ context.Context, obj runtime.Object) error { + r, ok := obj.(*AWSManagedControlPlaneTemplate) + if !ok { + return fmt.Errorf("expected an AWSManagedControlPlaneTemplate object but got %T", r) + } + + templateLog.Info("AWSManagedControlPlaneTemplate setting defaults", "control-plane", klog.KObj(r)) + + if r.Spec.Template.Spec.IdentityRef == nil { + r.Spec.Template.Spec.IdentityRef = &infrav1.AWSIdentityReference{ + Kind: infrav1.ControllerIdentityKind, + Name: infrav1.AWSClusterControllerIdentityName, + } + } + + infrav1.SetDefaults_Bastion(&r.Spec.Template.Spec.Bastion) + infrav1.SetDefaults_NetworkSpec(&r.Spec.Template.Spec.NetworkSpec) + + return nil +} + +func (r *AWSManagedControlPlaneTemplate) validateEKSVersion(old *AWSManagedControlPlaneTemplate) field.ErrorList { + path := field.NewPath("spec.template.spec.version") + var oldVersion *string + if old != nil { + oldVersion = old.Spec.Template.Spec.Version + } + return validateEKSVersion(r.Spec.Template.Spec.Version, oldVersion, r.Spec.Template.Spec.NetworkSpec, path) +} + +func (r *AWSManagedControlPlaneTemplate) validateIAMAuthConfig() field.ErrorList { + return validateIAMAuthConfig(r.Spec.Template.Spec.IAMAuthenticatorConfig, field.NewPath("spec.template.spec.iamAuthenticatorConfig")) +} + +func (r *AWSManagedControlPlaneTemplate) validateSecondaryCIDR() field.ErrorList { + return validateSecondaryCIDR(r.Spec.Template.Spec.SecondaryCidrBlock, field.NewPath("spec", "template", "spec", "secondaryCidrBlock")) +} + +func (r *AWSManagedControlPlaneTemplate) validateEKSAddons() field.ErrorList { + return validateEKSAddons(r.Spec.Template.Spec.Version, r.Spec.Template.Spec.NetworkSpec, r.Spec.Template.Spec.Addons, field.NewPath("spec.template.spec")) +} + +func (r *AWSManagedControlPlaneTemplate) validateDisableVPCCNI() field.ErrorList { + return validateDisableVPCCNI(r.Spec.Template.Spec.VpcCni, r.Spec.Template.Spec.Addons, field.NewPath("spec.template.spec")) +} + +func (r *AWSManagedControlPlaneTemplate) validateRestrictPrivateSubnets() field.ErrorList { + return validateRestrictPrivateSubnets(r.Spec.Template.Spec.RestrictPrivateSubnets, r.Spec.Template.Spec.NetworkSpec, "", field.NewPath("spec.template.spec")) +} + +func (r *AWSManagedControlPlaneTemplate) validateKubeProxy() field.ErrorList { + return validateKubeProxy(r.Spec.Template.Spec.KubeProxy, r.Spec.Template.Spec.Addons, field.NewPath("spec.template.spec")) +} + +func (r *AWSManagedControlPlaneTemplate) validateNetwork() field.ErrorList { + return validateNetwork("AWSManagedControlPlaneTemplate", r.Spec.Template.Spec.NetworkSpec, r.Spec.Template.Spec.SecondaryCidrBlock, field.NewPath("spec.template.spec")) +} + +func (r *AWSManagedControlPlaneTemplate) validatePrivateDNSHostnameTypeOnLaunch() field.ErrorList { + return validatePrivateDNSHostnameTypeOnLaunch(r.Spec.Template.Spec.NetworkSpec, field.NewPath("spec.template.spec")) +} diff --git a/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go b/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go index 216357e2fe..807613dc0d 100644 --- a/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go +++ b/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go @@ -243,6 +243,96 @@ func (in *AWSManagedControlPlaneStatus) DeepCopy() *AWSManagedControlPlaneStatus return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedControlPlaneTemplate) DeepCopyInto(out *AWSManagedControlPlaneTemplate) { + *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 AWSManagedControlPlaneTemplate. +func (in *AWSManagedControlPlaneTemplate) DeepCopy() *AWSManagedControlPlaneTemplate { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedControlPlaneTemplate) 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 *AWSManagedControlPlaneTemplateList) DeepCopyInto(out *AWSManagedControlPlaneTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AWSManagedControlPlaneTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateList. +func (in *AWSManagedControlPlaneTemplateList) DeepCopy() *AWSManagedControlPlaneTemplateList { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedControlPlaneTemplateList) 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 *AWSManagedControlPlaneTemplateResource) DeepCopyInto(out *AWSManagedControlPlaneTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateResource. +func (in *AWSManagedControlPlaneTemplateResource) DeepCopy() *AWSManagedControlPlaneTemplateResource { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedControlPlaneTemplateSpec) DeepCopyInto(out *AWSManagedControlPlaneTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateSpec. +func (in *AWSManagedControlPlaneTemplateSpec) DeepCopy() *AWSManagedControlPlaneTemplateSpec { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Addon) DeepCopyInto(out *Addon) { *out = *in diff --git a/main.go b/main.go index 8d5e4ebc88..41cc5abb6b 100644 --- a/main.go +++ b/main.go @@ -512,6 +512,11 @@ func setupEKSReconcilersAndWebhooks(ctx context.Context, mgr ctrl.Manager, awsSe setupLog.Error(err, "unable to create webhook", "webhook", "AWSManagedControlPlane") os.Exit(1) } + + if err := (&ekscontrolplanev1.AWSManagedControlPlaneTemplate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "AWSManagedControlPlaneTemplate") + os.Exit(1) + } } func initFlags(fs *pflag.FlagSet) { diff --git a/templates/cluster-template-eks-clusterclass.yaml b/templates/cluster-template-eks-clusterclass.yaml new file mode 100644 index 0000000000..d6c6d26823 --- /dev/null +++ b/templates/cluster-template-eks-clusterclass.yaml @@ -0,0 +1,66 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: ${CLUSTER_CLASS_NAME} +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: AWSManagedControlPlaneTemplate + name: "${CLUSTER_CLASS_NAME}-control-plane" + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: AWSManagedClusterTemplate + name: "${CLUSTER_CLASS_NAME}" + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + name: "${CLUSTER_CLASS_NAME}-md-0" + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: EKSConfigTemplate + infrastructure: + ref: + name: "${CLUSTER_CLASS_NAME}-md-0" + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: AWSMachineTemplate +--- +kind: AWSManagedClusterTemplate +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +metadata: + name: "${CLUSTER_CLASS_NAME}" +spec: + template: + spec: {} +--- +kind: AWSManagedControlPlaneTemplate +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 +metadata: + name: "${CLUSTER_CLASS_NAME}-control-plane" +spec: + template: + spec: + region: "${AWS_REGION}" + sshKeyName: "${AWS_SSH_KEY_NAME}" + version: "${KUBERNETES_VERSION}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: AWSMachineTemplate +metadata: + name: "${CLUSTER_CLASS_NAME}-md-0" +spec: + template: + spec: + instanceType: "${AWS_NODE_MACHINE_TYPE}" + iamInstanceProfile: "nodes.cluster-api-provider-aws.sigs.k8s.io" + sshKeyName: "${AWS_SSH_KEY_NAME}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 +kind: EKSConfigTemplate +metadata: + name: "${CLUSTER_CLASS_NAME}-md-0" +spec: + template: {} diff --git a/test/e2e/data/e2e_eks_conf.yaml b/test/e2e/data/e2e_eks_conf.yaml index 173223b15e..e88379507a 100644 --- a/test/e2e/data/e2e_eks_conf.yaml +++ b/test/e2e/data/e2e_eks_conf.yaml @@ -116,6 +116,8 @@ providers: targetName: "cluster-template-eks-control-plane-only-legacy.yaml" - sourcePath: "./eks/cluster-template-eks-control-plane-bare-eks.yaml" targetName: "cluster-template-eks-control-plane-bare-eks.yaml" + - sourcePath: "./infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/clusterclass-eks-e2e.yaml" + - sourcePath: "./infrastructure-aws/withclusterclass/generated/cluster-template-eks-clusterclass.yaml" variables: KUBERNETES_VERSION: "v1.32.0" @@ -124,6 +126,7 @@ variables: UPGRADE_FROM_VERSION: "v1.31.0" EXP_MACHINE_POOL: "true" EXP_CLUSTER_RESOURCE_SET: "true" + CLUSTER_TOPOLOGY: "true" EVENT_BRIDGE_INSTANCE_STATE: "true" AWS_NODE_MACHINE_TYPE: t3.large AWS_MACHINE_TYPE_VCPU_USAGE: 2 diff --git a/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/cluster-template.yaml b/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/cluster-template.yaml new file mode 100644 index 0000000000..d52ebc7a7b --- /dev/null +++ b/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/cluster-template.yaml @@ -0,0 +1,17 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "${CLUSTER_NAME}" +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + topology: + class: eks-e2e + version: "${KUBERNETES_VERSION}" + workers: + machineDeployments: + - class: default-worker + name: md-0 + replicas: ${WORKER_MACHINE_COUNT} diff --git a/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/clusterclass-eks-e2e.yaml b/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/clusterclass-eks-e2e.yaml new file mode 100644 index 0000000000..24fe63b4e9 --- /dev/null +++ b/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/clusterclass-eks-e2e.yaml @@ -0,0 +1,66 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: eks-e2e +spec: + controlPlane: + ref: + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 + kind: AWSManagedControlPlaneTemplate + name: "${CLUSTER_NAME}-control-plane" + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: AWSManagedClusterTemplate + name: "${CLUSTER_NAME}" + workers: + machineDeployments: + - class: default-worker + template: + bootstrap: + ref: + name: "${CLUSTER_NAME}-md-0" + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 + kind: EKSConfigTemplate + infrastructure: + ref: + name: "${CLUSTER_NAME}-md-0" + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: AWSMachineTemplate +--- +kind: AWSManagedClusterTemplate +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +metadata: + name: "${CLUSTER_NAME}" +spec: + template: + spec: {} +--- +kind: AWSManagedControlPlaneTemplate +apiVersion: controlplane.cluster.x-k8s.io/v1beta2 +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + template: + spec: + region: "${AWS_REGION}" + sshKeyName: "${AWS_SSH_KEY_NAME}" + version: "${KUBERNETES_VERSION}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: AWSMachineTemplate +metadata: + name: "${CLUSTER_NAME}-md-0" +spec: + template: + spec: + instanceType: "${AWS_NODE_MACHINE_TYPE}" + iamInstanceProfile: "nodes.cluster-api-provider-aws.sigs.k8s.io" + sshKeyName: "${AWS_SSH_KEY_NAME}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 +kind: EKSConfigTemplate +metadata: + name: "${CLUSTER_NAME}-md-0" +spec: + template: {} diff --git a/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/kustomization.yaml b/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/kustomization.yaml new file mode 100644 index 0000000000..4b1ea9970c --- /dev/null +++ b/test/e2e/data/infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/kustomization.yaml @@ -0,0 +1,9 @@ +resources: + - cluster-template.yaml +generatorOptions: + disableNameSuffixHash: true + labels: + type: generated + annotations: + note: generated + diff --git a/test/e2e/suites/managed/control_plane_helpers.go b/test/e2e/suites/managed/control_plane_helpers.go index 11b72198b3..0178236d32 100644 --- a/test/e2e/suites/managed/control_plane_helpers.go +++ b/test/e2e/suites/managed/control_plane_helpers.go @@ -28,11 +28,14 @@ import ( ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/version" crclient "sigs.k8s.io/controller-runtime/pkg/client" ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/test/framework" + clusterctl "sigs.k8s.io/cluster-api/test/framework/clusterctl" ) type waitForControlPlaneToBeUpgradedInput struct { @@ -93,3 +96,73 @@ func GetControlPlaneByName(ctx context.Context, input GetControlPlaneByNameInput Expect(input.Getter.Get(ctx, key, cp)).To(Succeed(), "Failed to get AWSManagedControlPlane object %s/%s", input.Namespace, input.Name) return cp } + +func WaitForEKSControlPlaneInitialized(ctx context.Context, input clusterctl.ApplyCustomClusterTemplateAndWaitInput, result *clusterctl.ApplyCustomClusterTemplateAndWaitResult) { + Expect(ctx).NotTo(BeNil(), "ctx is required for WaitForEKSControlPlaneInitialized") + Expect(input.ClusterProxy).ToNot(BeNil(), "Invalid argument. input.ClusterProxy can't be nil") + + var awsCP ekscontrolplanev1.AWSManagedControlPlane + Eventually(func(g Gomega) { + list, err := listAWSManagedControlPlanes(ctx, input.ClusterProxy.GetClient(), result.Cluster.Namespace, result.Cluster.Name) + g.Expect(err).To(Succeed(), "failed to list AWSManagedControlPlane resource") + + g.Expect(len(list.Items)).To(Equal(1), + "expected exactly one AWSManagedControlPlane for %s/%s", + result.Cluster.Namespace, result.Cluster.Name, + ) + awsCP = list.Items[0] + }, 10*time.Second, 1*time.Second).Should(Succeed()) + + key := crclient.ObjectKey{Namespace: awsCP.Namespace, Name: awsCP.Name} + waitForControlPlaneReady(ctx, input.ClusterProxy.GetClient(), key, input.WaitForControlPlaneIntervals...) +} + +func WaitForEKSControlPlaneMachinesReady(ctx context.Context, input clusterctl.ApplyCustomClusterTemplateAndWaitInput, result *clusterctl.ApplyCustomClusterTemplateAndWaitResult) { + Expect(ctx).NotTo(BeNil(), "ctx is required for WaitForEKSControlPlaneMachinesReady") + Expect(input.ClusterProxy).ToNot(BeNil(), "input.ClusterProxy can't be nil") + + var awsCP ekscontrolplanev1.AWSManagedControlPlane + Eventually(func(g Gomega) { + list, err := listAWSManagedControlPlanes(ctx, input.ClusterProxy.GetClient(), result.Cluster.Namespace, result.Cluster.Name) + g.Expect(err).To(Succeed()) + awsCP = list.Items[0] + + g.Expect(awsCP.Status.Ready).To(BeTrue(), + "waiting for AWSManagedControlPlane %s/%s to become Ready", + awsCP.Namespace, awsCP.Name, + ) + }, input.WaitForControlPlaneIntervals...).Should(Succeed()) + + workloadClusterProxy := input.ClusterProxy.GetWorkloadCluster(ctx, result.Cluster.Namespace, input.ClusterName) + waitForWorkloadClusterReachable(ctx, workloadClusterProxy.GetClient(), input.WaitForControlPlaneIntervals...) +} + +// listAWSManagedControlPlanes returns a list of AWSManagedControlPlanes for the given cluster. +func listAWSManagedControlPlanes(ctx context.Context, client crclient.Client, namespace, clusterName string) (*ekscontrolplanev1.AWSManagedControlPlaneList, error) { + list := &ekscontrolplanev1.AWSManagedControlPlaneList{} + err := client.List(ctx, list, + crclient.InNamespace(namespace), + crclient.MatchingLabels{clusterv1.ClusterNameLabel: clusterName}, + ) + return list, err +} + +// waitForControlPlaneReady polls until the given AWSManagedControlPlane is marked Ready. +func waitForControlPlaneReady(ctx context.Context, client crclient.Client, key crclient.ObjectKey, intervals ...interface{}) { + Eventually(func(g Gomega) { + var latest ekscontrolplanev1.AWSManagedControlPlane + g.Expect(client.Get(ctx, key, &latest)).To(Succeed()) + g.Expect(latest.Status.Ready).To(BeTrue(), + "AWSManagedControlPlane %s/%s is not Ready", key.Namespace, key.Name, + ) + }, intervals...).Should(Succeed()) +} + +// waitForWorkloadClusterReachable checks when the kube-system namespace is reachable in the workload cluster. +func waitForWorkloadClusterReachable(ctx context.Context, client crclient.Client, intervals ...interface{}) { + Eventually(func(g Gomega) { + ns := &corev1.Namespace{} + g.Expect(client.Get(ctx, crclient.ObjectKey{Name: "kube-system"}, ns)). + To(Succeed(), "workload API server not yet reachable") + }, intervals...).Should(Succeed()) +} diff --git a/test/e2e/suites/managed/eks_clusterclass_test.go b/test/e2e/suites/managed/eks_clusterclass_test.go new file mode 100644 index 0000000000..3c492aaee9 --- /dev/null +++ b/test/e2e/suites/managed/eks_clusterclass_test.go @@ -0,0 +1,79 @@ +//go:build e2e +// +build e2e + +/* +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 managed + +import ( + "context" + "fmt" + + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/utils/ptr" + + ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2" + "sigs.k8s.io/cluster-api-provider-aws/v2/test/e2e/shared" + capi_e2e "sigs.k8s.io/cluster-api/test/e2e" + "sigs.k8s.io/cluster-api/test/framework/clusterctl" + "sigs.k8s.io/cluster-api/util" +) + +var _ = ginkgo.Describe("[managed] [general] EKS clusterclass tests", func() { + const specName = "cluster" + var ( + ctx context.Context + clusterName string + ) + + ginkgo.BeforeEach(func() { + ctx = context.TODO() + + if !runGeneralTests() { + ginkgo.Skip("skipping due to unmet condition") + } + + ginkgo.By("should have a valid test configuration") + Expect(e2eCtx.Environment.BootstrapClusterProxy).ToNot(BeNil(), "BootstrapClusterProxy can't be nil") + Expect(e2eCtx.E2EConfig).ToNot(BeNil(), "E2EConfig can't be nil") + Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.KubernetesVersion)) + Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.CNIAddonVersion)) + + clusterName = fmt.Sprintf("%s-%s", specName, util.RandomString(6)) + + ginkgo.By("default iam role should exist") + VerifyRoleExistsAndOwned(ctx, ekscontrolplanev1.DefaultEKSControlPlaneRole, "", false, e2eCtx.AWSSessionV2) + }) + + capi_e2e.QuickStartSpec(context.TODO(), func() capi_e2e.QuickStartSpecInput { + return capi_e2e.QuickStartSpecInput{ + E2EConfig: e2eCtx.E2EConfig, + ClusterctlConfigPath: e2eCtx.Environment.ClusterctlConfigPath, + BootstrapClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + ArtifactFolder: e2eCtx.Settings.ArtifactFolder, + SkipCleanup: e2eCtx.Settings.SkipCleanup, + Flavor: ptr.To(EKSClusterClassFlavor), + ClusterName: ptr.To(clusterName), + WorkerMachineCount: ptr.To(int64(3)), + ControlPlaneWaiters: clusterctl.ControlPlaneWaiters{ + WaitForControlPlaneInitialized: WaitForEKSControlPlaneInitialized, + WaitForControlPlaneMachinesReady: WaitForEKSControlPlaneMachinesReady, + }, + } + }) +}) diff --git a/test/e2e/suites/managed/helpers.go b/test/e2e/suites/managed/helpers.go index f6c5a942a0..2922c70201 100644 --- a/test/e2e/suites/managed/helpers.go +++ b/test/e2e/suites/managed/helpers.go @@ -49,6 +49,7 @@ const ( EKSMachinePoolOnlyFlavor = "eks-machinepool-only" EKSIPv6ClusterFlavor = "eks-ipv6-cluster" EKSControlPlaneOnlyLegacyFlavor = "eks-control-plane-only-legacy" + EKSClusterClassFlavor = "eks-clusterclass" ) const (