diff --git a/Makefile b/Makefile index a6cd1c6b4..3d6138d26 100644 --- a/Makefile +++ b/Makefile @@ -329,6 +329,7 @@ generate-go: $(CONTROLLER_GEN) $(CONVERSION_GEN) ## Runs Go related generate tar paths=./ \ paths=./... \ paths=./$(EXP_DIR)/api/... \ + paths=./$(EXP_DIR)/bootstrap/gke/api/... \ object:headerFile=./hack/boilerplate/boilerplate.generatego.txt go generate ./... @@ -338,6 +339,7 @@ generate-manifests: $(CONTROLLER_GEN) ## Generate manifests e.g. CRD, RBAC etc. paths=./ \ paths=./api/... \ paths=./$(EXP_DIR)/api/... \ + paths=./$(EXP_DIR)/bootstrap/gke/api/... \ crd:crdVersions=v1 \ rbac:roleName=manager-role \ output:crd:dir=$(CRD_ROOT) \ @@ -347,6 +349,7 @@ generate-manifests: $(CONTROLLER_GEN) ## Generate manifests e.g. CRD, RBAC etc. paths=./ \ paths=./controllers/... \ paths=./$(EXP_DIR)/controllers/... \ + paths=./$(EXP_DIR)/bootstrap/gke/controllers/... \ output:rbac:dir=$(RBAC_ROOT) \ rbac:roleName=manager-role @@ -622,5 +625,7 @@ verify-modules: modules .PHONY: verify-gen verify-gen: generate @if !(git diff --quiet HEAD); then \ - echo "generated files are out of date, run make generate"; exit 1; \ + echo "generated files are out of date, run make generate"; \ + git diff HEAD; \ + exit 1; \ fi diff --git a/cloud/scope/managedmachinepool.go b/cloud/scope/managedmachinepool.go index e2a4577a4..1f4f5fc83 100644 --- a/cloud/scope/managedmachinepool.go +++ b/cloud/scope/managedmachinepool.go @@ -312,3 +312,15 @@ func (s *ManagedMachinePoolScope) NodePoolLocation() string { func (s *ManagedMachinePoolScope) NodePoolFullName() string { return fmt.Sprintf("%s/nodePools/%s", s.NodePoolLocation(), s.NodePoolName()) } + +// SetInfrastructureMachineKind sets the infrastructure machine kind in the status if it is not set already, returning +// `true` if the status was updated. This supports MachinePool Machines. +func (s *ManagedMachinePoolScope) SetInfrastructureMachineKind() bool { + if s.GCPManagedMachinePool.Status.InfrastructureMachineKind != infrav1exp.GCPManagedMachinePoolMachineKind { + s.GCPManagedMachinePool.Status.InfrastructureMachineKind = infrav1exp.GCPManagedMachinePoolMachineKind + + return true + } + + return false +} diff --git a/cloud/scope/managedmachinepool_test.go b/cloud/scope/managedmachinepool_test.go index b8fa01dd0..ba1c87cb2 100644 --- a/cloud/scope/managedmachinepool_test.go +++ b/cloud/scope/managedmachinepool_test.go @@ -31,7 +31,9 @@ var _ = Describe("GCPManagedMachinePool Scope", func() { Namespace: namespace, }, Spec: v1beta1.GCPManagedMachinePoolSpec{ - NodePoolName: nodePoolName, + GCPManagedMachinePoolClassSpec: v1beta1.GCPManagedMachinePoolClassSpec{ + NodePoolName: nodePoolName, + }, }, } TestMP = &clusterv1exp.MachinePool{ @@ -52,6 +54,32 @@ var _ = Describe("GCPManagedMachinePool Scope", func() { }) }) + Context("Test MachinePool InfrastructureMachineKind", func() { + It("should set infrastructure machine kind when empty", func() { + TestGCPMMP.Status = v1beta1.GCPManagedMachinePoolStatus{} + machinePoolScope := ManagedMachinePoolScope{ + GCPManagedMachinePool: TestGCPMMP, + } + + update := machinePoolScope.SetInfrastructureMachineKind() + Expect(machinePoolScope.GCPManagedMachinePool.Status.InfrastructureMachineKind).To(Equal(v1beta1.GCPManagedMachinePoolMachineKind)) + Expect(update).To(BeTrue()) + }) + + It("should not update infrastructure machine kind if already set", func() { + TestGCPMMP.Status = v1beta1.GCPManagedMachinePoolStatus{ + InfrastructureMachineKind: v1beta1.GCPManagedMachinePoolMachineKind, + } + machinePoolScope := ManagedMachinePoolScope{ + GCPManagedMachinePool: TestGCPMMP, + } + + update := machinePoolScope.SetInfrastructureMachineKind() + Expect(machinePoolScope.GCPManagedMachinePool.Status.InfrastructureMachineKind).To(Equal(v1beta1.GCPManagedMachinePoolMachineKind)) + Expect(update).To(BeFalse()) + }) + }) + Context("Test ConvertToSdkNodePool", func() { It("should convert to SDK node pool with default values", func() { sdkNodePool := ConvertToSdkNodePool(*TestGCPMMP, *TestMP, false, TestClusterName) diff --git a/config/crd/bases/bootstrap.cluster.x-k8s.io_gkeconfigs.yaml b/config/crd/bases/bootstrap.cluster.x-k8s.io_gkeconfigs.yaml new file mode 100644 index 000000000..b6a3ec156 --- /dev/null +++ b/config/crd/bases/bootstrap.cluster.x-k8s.io_gkeconfigs.yaml @@ -0,0 +1,138 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: gkeconfigs.bootstrap.cluster.x-k8s.io +spec: + group: bootstrap.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: GKEConfig + listKind: GKEConfigList + plural: gkeconfigs + shortNames: + - gkec + singular: gkeconfig + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Bootstrap configuration is ready + jsonPath: .status.ready + name: Ready + type: string + - description: Name of Secret containing bootstrap data + jsonPath: .status.dataSecretName + name: DataSecretName + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: |- + GKEConfig is the schema for the GCP GKE Bootstrap Configuration. + this is a placeholder used for compliance with the CAPI contract. + 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: GKEConfigSpec defines the desired state of GCP GKE Bootstrap + Configuration. + type: object + status: + description: GKEConfigStatus defines the observed state of the GCP GKE + Bootstrap Configuration. + properties: + conditions: + description: Conditions defines current service state of the GKEConfig. + items: + description: Condition defines an observation of a Cluster API resource + operational state. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when + the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This field may be empty. + maxLength: 10240 + minLength: 1 + type: string + reason: + description: |- + reason is the reason for the condition's last transition in CamelCase. + The specific API may choose whether or not this field is considered a guaranteed API. + This field may be empty. + maxLength: 256 + minLength: 1 + type: string + severity: + description: |- + severity provides an explicit classification of Reason code, so the users or machines can immediately + understand the current situation and act accordingly. + The Severity field MUST be set only when Status=False. + maxLength: 32 + type: string + status: + description: status of the condition, one of True, False, Unknown. + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions + can be useful (see .node.status.conditions), the ability to deconflict is important. + maxLength: 256 + minLength: 1 + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + dataSecretName: + description: DataSecretName is the name of the secret that stores + the bootstrap data script. + type: string + failureMessage: + description: FailureMessage will be set on non-retryable errors + type: string + failureReason: + description: FailureReason will be set on non-retryable errors + type: string + observedGeneration: + description: ObservedGeneration is the latest generation observed + by the controller. + format: int64 + type: integer + ready: + description: Ready indicates the BootstrapData secret is ready to + be consumed + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/bootstrap.cluster.x-k8s.io_gkeconfigtemplates.yaml b/config/crd/bases/bootstrap.cluster.x-k8s.io_gkeconfigtemplates.yaml new file mode 100644 index 000000000..ff70effc2 --- /dev/null +++ b/config/crd/bases/bootstrap.cluster.x-k8s.io_gkeconfigtemplates.yaml @@ -0,0 +1,61 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: gkeconfigtemplates.bootstrap.cluster.x-k8s.io +spec: + group: bootstrap.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: GKEConfigTemplate + listKind: GKEConfigTemplateList + plural: gkeconfigtemplates + shortNames: + - gkect + singular: gkeconfigtemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GKEConfigTemplate is the GCP GKE Bootstrap Configuration Template + 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: GKEConfigTemplateSpec defines the desired state of templated + GKEConfig GCP GKE Bootstrap Configuration resources. + properties: + template: + description: GKEConfigTemplateResource defines the Template structure. + properties: + spec: + description: GKEConfigSpec defines the desired state of GCP GKE + Bootstrap Configuration. + type: object + type: object + required: + - template + type: object + type: object + served: true + storage: true diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclustertemplates.yaml new file mode 100644 index 000000000..d527cb3f2 --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclustertemplates.yaml @@ -0,0 +1,381 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: gcpmanagedclustertemplates.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: GCPManagedClusterTemplate + listKind: GCPManagedClusterTemplateList + plural: gcpmanagedclustertemplates + shortNames: + - amct + singular: gcpmanagedclustertemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GCPManagedClusterTemplate is the Schema for the GCPManagedClusterTemplates + 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: GCPManagedClusterTemplateSpec defines the desired state of + GCPManagedClusterTemplate. + properties: + template: + description: GCPManagedClusterTemplateResource describes the data + needed to create an GCPManagedCluster from a template. + properties: + spec: + description: GCPManagedClusterTemplateResourceSpec specifies an + GCP managed cluster template resource. + properties: + additionalLabels: + additionalProperties: + type: string + description: |- + AdditionalLabels is an optional set of tags to add to GCP resources managed by the GCP provider, in addition to the + ones added by default. + type: object + 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 + credentialsRef: + description: |- + CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this cluster. If not + supplied then the credentials of the controller will be used. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + required: + - name + - namespace + type: object + loadBalancer: + description: LoadBalancerSpec contains configuration for one + or more LoadBalancers. + properties: + apiServerInstanceGroupTagOverride: + description: |- + APIServerInstanceGroupTagOverride overrides the default setting for the + tag used when creating the API Server Instance Group. + maxLength: 16 + pattern: (^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) + type: string + internalLoadBalancer: + description: InternalLoadBalancer is the configuration + for an Internal Passthrough Network Load Balancer. + properties: + internalAccess: + default: Regional + description: |- + InternalAccess defines the access for the Internal Passthrough Load Balancer. + It determines whether the load balancer allows global access, + or restricts traffic to clients within the same region as the load balancer. + If unspecified, the value defaults to "Regional". + + Possible values: + "Regional" - Only clients in the same region as the load balancer can access it. + "Global" - Clients from any region can access the load balancer. + enum: + - Regional + - Global + type: string + ipAddress: + description: |- + IPAddress is the static IP address to use for the Load Balancer. + If not set, a new static IP address will be allocated. + If set, it must be a valid free IP address from the LoadBalancer Subnet. + type: string + name: + description: |- + Name is the name of the Load Balancer. If not set a default name + will be used. For an Internal Load Balancer service the default + name is "api-internal". + pattern: (^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) + type: string + subnet: + description: |- + Subnet is the name of the subnet to use for a regional Load Balancer. A subnet is + required for the Load Balancer, if not defined the first configured subnet will be + used. + type: string + type: object + loadBalancerType: + description: |- + LoadBalancerType defines the type of Load Balancer that should be created. + If not set, a Global External Proxy Load Balancer will be created by default. + type: string + type: object + network: + description: NetworkSpec encapsulates all things related to + the GCP network. + properties: + autoCreateSubnetworks: + description: |- + AutoCreateSubnetworks: When set to true, the VPC network is created + in "auto" mode. When set to false, the VPC network is created in + "custom" mode. + + An auto mode VPC network starts with one subnet per region. Each + subnet has a predetermined range as described in Auto mode VPC + network IP ranges. + + Defaults to true. + type: boolean + hostProject: + description: HostProject is the name of the project hosting + the shared VPC network resources. + type: string + loadBalancerBackendPort: + description: Allow for configuration of load balancer + backend (useful for changing apiserver port) + format: int32 + type: integer + mtu: + default: 1460 + description: |- + Mtu: Maximum Transmission Unit in bytes. The minimum value for this field is + 1300 and the maximum value is 8896. The suggested value is 1500, which is + the default MTU used on the Internet, or 8896 if you want to use Jumbo + frames. If unspecified, the value defaults to 1460. + More info: https://pkg.go.dev/google.golang.org/api/compute/v1#Network + format: int64 + maximum: 8896 + minimum: 1300 + type: integer + name: + description: Name is the name of the network to be used. + type: string + subnets: + description: Subnets configuration. + items: + description: SubnetSpec configures an GCP Subnet. + properties: + cidrBlock: + description: |- + CidrBlock is the range of internal addresses that are owned by this + subnetwork. Provide this property when you create the subnetwork. For + example, 10.0.0.0/8 or 192.168.0.0/16. Ranges must be unique and + non-overlapping within a network. Only IPv4 is supported. This field + can be set only at resource creation time. + type: string + description: + description: Description is an optional description + associated with the resource. + type: string + enableFlowLogs: + description: |- + EnableFlowLogs: Whether to enable flow logging for this subnetwork. + If this field is not explicitly set, it will not appear in get + listings. If not set the default behavior is to disable flow logging. + type: boolean + name: + description: Name defines a unique identifier to + reference this resource. + type: string + privateGoogleAccess: + description: |- + PrivateGoogleAccess defines whether VMs in this subnet can access + Google services without assigning external IP addresses + type: boolean + purpose: + default: PRIVATE_RFC_1918 + description: |- + Purpose: The purpose of the resource. + If unspecified, the purpose defaults to PRIVATE_RFC_1918. + The enableFlowLogs field isn't supported with the purpose field set to INTERNAL_HTTPS_LOAD_BALANCER. + + Possible values: + "INTERNAL_HTTPS_LOAD_BALANCER" - Subnet reserved for Internal + HTTP(S) Load Balancing. + "PRIVATE" - Regular user created or automatically created subnet. + "PRIVATE_RFC_1918" - Regular user created or automatically created + subnet. + "PRIVATE_SERVICE_CONNECT" - Subnetworks created for Private Service + Connect in the producer network. + "REGIONAL_MANAGED_PROXY" - Subnetwork used for Regional + Internal/External HTTP(S) Load Balancing. + enum: + - INTERNAL_HTTPS_LOAD_BALANCER + - PRIVATE_RFC_1918 + - PRIVATE + - PRIVATE_SERVICE_CONNECT + - REGIONAL_MANAGED_PROXY + type: string + region: + description: Region is the name of the region where + the Subnetwork resides. + type: string + secondaryCidrBlocks: + additionalProperties: + type: string + description: |- + SecondaryCidrBlocks defines secondary CIDR ranges, + from which secondary IP ranges of a VM may be allocated + type: object + stackType: + default: IPV4_ONLY + description: |- + StackType: The stack type for the subnet. If set to IPV4_ONLY, new VMs in + the subnet are assigned IPv4 addresses only. If set to IPV4_IPV6, new VMs in + the subnet can be assigned both IPv4 and IPv6 addresses. If not specified, + IPV4_ONLY is used. This field can be both set at resource creation time and + updated using patch. + + Possible values: + "IPV4_IPV6" - New VMs in this subnet can have both IPv4 and IPv6 + addresses. + "IPV4_ONLY" - New VMs in this subnet will only be assigned IPv4 addresses. + "IPV6_ONLY" - New VMs in this subnet will only be assigned IPv6 addresses. + enum: + - IPV4_ONLY + - IPV4_IPV6 + - IPV6_ONLY + type: string + type: object + type: array + type: object + project: + description: Project is the name of the project to deploy + the cluster to. + type: string + region: + description: The GCP Region the cluster lives in. + type: string + resourceManagerTags: + description: |- + ResourceManagerTags is an optional set of tags to apply to GCP resources managed + by the GCP provider. GCP supports a maximum of 50 tags per resource. + items: + description: ResourceManagerTag is a tag to apply to GCP + resources managed by the GCP provider. + properties: + key: + description: |- + Key is the key part of the tag. A tag key can have a maximum of 63 characters and cannot + be empty. Tag key must begin and end with an alphanumeric character, and must contain + only uppercase, lowercase alphanumeric characters, and the following special + characters `._-`. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$ + type: string + parentID: + description: |- + ParentID is the ID of the hierarchical resource where the tags are defined + e.g. at the Organization or the Project level. To find the Organization or Project ID ref + https://cloud.google.com/resource-manager/docs/creating-managing-organization#retrieving_your_organization_id + https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects + An OrganizationID must consist of decimal numbers, and cannot have leading zeroes. + A ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, + numbers, and hyphens, and must start with a letter, and cannot end with a hyphen. + maxLength: 32 + minLength: 1 + pattern: (^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) + type: string + value: + description: |- + Value is the value part of the tag. A tag value can have a maximum of 63 characters and + cannot be empty. Tag value must begin and end with an alphanumeric character, and must + contain only uppercase, lowercase alphanumeric characters, and the following special + characters `_-.@%=+:,*#&(){}[]` and spaces. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\[\]{}\-\s]{0,61}[a-zA-Z0-9])?$ + type: string + required: + - key + - parentID + - value + type: object + type: array + serviceEndpoints: + description: |- + ServiceEndpoints contains the custom GCP Service Endpoint urls for each applicable service. + For instance, the user can specify a new endpoint for the compute service. + properties: + compute: + description: ComputeServiceEndpoint is the custom endpoint + url for the Compute Service + format: uri + pattern: ^https:// + type: string + container: + description: ContainerServiceEndpoint is the custom endpoint + url for the Container Service + format: uri + pattern: ^https:// + type: string + iam: + description: IAMServiceEndpoint is the custom endpoint + url for the IAM Service + format: uri + pattern: ^https:// + type: string + resourceManager: + description: ResourceManagerServiceEndpoint is the custom + endpoint url for the Resource Manager Service + format: uri + pattern: ^https:// + type: string + type: object + required: + - project + - region + 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_gcpmanagedcontrolplanes.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml index 1b8d13225..5aed53795 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml @@ -178,6 +178,13 @@ spec: Possible values: none, logging.googleapis.com/kubernetes (default). Value is ignored when enableAutopilot = true. type: string + machineTemplate: + description: |- + MachineTemplate contains information about how machines + should be shaped when creating or updating a control plane. + For the GCPManagedControlPlaneTemplate, this field is used + only to fulfill the CAPI contract. + type: object master_authorized_networks_config: description: |- MasterAuthorizedNetworksConfig represents configuration options for master authorized networks feature of the GKE cluster. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanetemplates.yaml new file mode 100644 index 000000000..6e74a0776 --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanetemplates.yaml @@ -0,0 +1,203 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: gcpmanagedcontrolplanetemplates.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: GCPManagedControlPlaneTemplate + listKind: GCPManagedControlPlaneTemplateList + plural: gcpmanagedcontrolplanetemplates + shortNames: + - amcpt + singular: gcpmanagedcontrolplanetemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GCPManagedControlPlaneTemplate is the Schema for the GCPManagedControlPlaneTemplates + 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: GCPManagedControlPlaneTemplateSpec defines the desired state + of GCPManagedControlPlaneTemplate. + properties: + template: + description: GCPManagedControlPlaneTemplateResource describes the + data needed to create an GCPManagedCluster from a template. + properties: + spec: + description: GCPManagedControlPlaneTemplateResourceSpec specifies + an GCP managed control plane template resource. + properties: + clusterNetwork: + description: ClusterNetwork define the cluster network. + properties: + pod: + description: Pod defines the range of CIDRBlock list from + where it gets the IP address. + properties: + cidrBlock: + description: |- + CidrBlock is where all pods in the cluster are assigned an IP address from this range. Enter a range + (in CIDR notation) within a network range, a mask, or leave this field blank to use a default range. + This setting is permanent. + type: string + type: object + privateCluster: + description: PrivateCluster defines the private cluster + spec. + properties: + controlPlaneCidrBlock: + description: |- + ControlPlaneCidrBlock is the IP range in CIDR notation to use for the hosted master network. This range must not + overlap with any other ranges in use within the cluster's network. Honored when enabled is true. + type: string + controlPlaneGlobalAccess: + description: ControlPlaneGlobalAccess is whenever + master is accessible globally or not. Honored when + enabled is true. + type: boolean + disableDefaultSNAT: + description: DisableDefaultSNAT disables cluster default + sNAT rules. Honored when enabled is true. + type: boolean + enablePrivateEndpoint: + description: |- + EnablePrivateEndpoint: Whether the master's internal IP + address is used as the cluster endpoint. + type: boolean + enablePrivateNodes: + description: |- + EnablePrivateNodes: Whether nodes have internal IP + addresses only. If enabled, all nodes are given only RFC + 1918 private addresses and communicate with the master via + private networking. + type: boolean + type: object + service: + description: Service defines the range of CIDRBlock list + from where it gets the IP address. + properties: + cidrBlock: + description: |- + CidrBlock is where cluster services will be assigned an IP address from this IP address range. Enter a range + (in CIDR notation) within a network range, a mask, or leave this field blank to use a default range. + This setting is permanent. + type: string + type: object + useIPAliases: + description: |- + UseIPAliases is whether alias IPs will be used for pod IPs in the cluster. If false, routes will be used for + pod IPs in the cluster. + type: boolean + type: object + enableAutopilot: + description: EnableAutopilot indicates whether to enable autopilot + for this GKE cluster. + type: boolean + enableIdentityService: + description: EnableIdentityService indicates whether to enable + Identity Service component for this GKE cluster. + type: boolean + location: + description: |- + Location represents the location (region or zone) in which the GKE cluster + will be created. + type: string + loggingService: + description: |- + LoggingService represents configuration of logging service feature of the GKE cluster. + Possible values: none, logging.googleapis.com/kubernetes (default). + Value is ignored when enableAutopilot = true. + type: string + machineTemplate: + description: |- + MachineTemplate contains information about how machines + should be shaped when creating or updating a control plane. + For the GCPManagedControlPlaneTemplate, this field is used + only to fulfill the CAPI contract. + type: object + master_authorized_networks_config: + description: |- + MasterAuthorizedNetworksConfig represents configuration options for master authorized networks feature of the GKE cluster. + This feature is disabled if this field is not specified. + properties: + cidr_blocks: + description: |- + cidr_blocks define up to 50 external networks that could access + Kubernetes master through HTTPS. + items: + description: MasterAuthorizedNetworksConfigCidrBlock + contains an optional name and one CIDR block. + properties: + cidr_block: + description: cidr_block must be specified in CIDR + notation. + pattern: ^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$|^([a-fA-F0-9:]+:+)+[a-fA-F0-9]+\/[0-9]{1,3}$ + type: string + display_name: + description: display_name is an field for users + to identify CIDR blocks. + type: string + type: object + type: array + gcp_public_cidrs_access_enabled: + description: Whether master is accessible via Google Compute + Engine Public IP addresses. + type: boolean + type: object + monitoringService: + description: |- + MonitoringService represents configuration of monitoring service feature of the GKE cluster. + Possible values: none, monitoring.googleapis.com/kubernetes (default). + Value is ignored when enableAutopilot = true. + type: string + project: + description: Project is the name of the project to deploy + the cluster to. + type: string + releaseChannel: + description: ReleaseChannel represents the release channel + of the GKE cluster. + enum: + - rapid + - regular + - stable + type: string + required: + - location + - project + 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_gcpmanagedmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepools.yaml index c8092a9ce..2e0e82271 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepools.yaml @@ -342,6 +342,10 @@ spec: - type type: object type: array + infrastructureMachineKind: + description: InfrastructureMachineKind is the kind of the infrastructure + resources behind MachinePool Machines. + type: string ready: default: false description: Ready denotes that the GCPManagedMachinePool has joined diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepooltemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepooltemplates.yaml new file mode 100644 index 000000000..d470788fb --- /dev/null +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepooltemplates.yaml @@ -0,0 +1,294 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: gcpmanagedmachinepooltemplates.infrastructure.cluster.x-k8s.io +spec: + group: infrastructure.cluster.x-k8s.io + names: + categories: + - cluster-api + kind: GCPManagedMachinePoolTemplate + listKind: GCPManagedMachinePoolTemplateList + plural: gcpmanagedmachinepooltemplates + shortNames: + - ammpt + singular: gcpmanagedmachinepooltemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: GCPManagedMachinePoolTemplate is the Schema for the GCPManagedMachinePoolTemplates + 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: GCPManagedMachinePoolTemplateSpec defines the desired state + of GCPManagedMachinePoolTemplate. + properties: + template: + description: GCPManagedMachinePoolTemplateResource describes the data + needed to create an GCPManagedCluster from a template. + properties: + spec: + description: GCPManagedMachinePoolTemplateResourceSpec specifies + an GCP managed control plane template resource. + properties: + additionalLabels: + additionalProperties: + type: string + description: |- + AdditionalLabels is an optional set of tags to add to GCP resources managed by the GCP provider, in addition to the + ones added by default. + type: object + diskSizeGB: + description: |- + DiskSizeGB is size of the disk attached to each node, + specified in GB. + format: int64 + minimum: 10 + type: integer + diskSizeGb: + description: |- + DiskSizeGb is the size of the disk attached to each node, specified in GB. + The smallest allowed disk size is 10GB. If unspecified, the default disk size is 100GB. + format: int32 + type: integer + diskType: + description: DiskType is type of the disk attached to each + node. + enum: + - pd-standard + - pd-ssd + - pd-balanced + type: string + imageType: + description: ImageType is image type to use for this nodepool. + type: string + instanceType: + description: InstanceType is name of Compute Engine machine + type. + type: string + kubernetesLabels: + additionalProperties: + type: string + description: KubernetesLabels specifies the labels to apply + to the nodes of the node pool. + type: object + kubernetesTaints: + description: KubernetesTaints specifies the taints to apply + to the nodes of the node pool. + items: + description: Taint represents a Kubernetes taint. + properties: + effect: + description: Effect specifies the effect for the taint. + enum: + - NoSchedule + - NoExecute + - PreferNoSchedule + type: string + key: + description: Key is the key of the taint + type: string + value: + description: Value is the value of the taint + type: string + required: + - effect + - key + - value + type: object + type: array + linuxNodeConfig: + description: LinuxNodeConfig specifies the settings for Linux + agent nodes. + properties: + cgroupMode: + description: CgroupMode specifies the cgroup mode for + this node pool. + format: int32 + type: integer + sysctls: + description: Sysctls specifies the sysctl settings for + this node pool. + items: + description: SysctlConfig specifies the sysctl settings + for Linux nodes. + properties: + parameter: + description: Parameter specifies sysctl parameter + name. + type: string + value: + description: Value specifies sysctl parameter value. + type: string + type: object + type: array + type: object + localSsdCount: + description: LocalSsdCount is the number of local SSD disks + to be attached to the node. + format: int32 + type: integer + machineType: + description: |- + MachineType is the name of a Google Compute Engine [machine + type](https://cloud.google.com/compute/docs/machine-types). + If unspecified, the default machine type is `e2-medium`. + type: string + management: + description: Management specifies the node pool management + options. + properties: + autoRepair: + description: |- + AutoRepair specifies whether the node auto-repair is enabled for the node + pool. If enabled, the nodes in this node pool will be monitored and, if + they fail health checks too many times, an automatic repair action will be + triggered. + type: boolean + autoUpgrade: + description: |- + AutoUpgrade specifies whether node auto-upgrade is enabled for the node + pool. If enabled, node auto-upgrade helps keep the nodes in your node pool + up to date with the latest release version of Kubernetes. + type: boolean + type: object + maxPodsPerNode: + description: |- + MaxPodsPerNode is constraint enforced on the max num of + pods per node. + format: int64 + maximum: 256 + minimum: 8 + type: integer + nodeLocations: + description: |- + NodeLocations is the list of zones in which the NodePool's + nodes should be located. + items: + type: string + type: array + nodeNetwork: + description: |- + NodeNetwork specifies the node network configuration + options. + properties: + createPodRange: + description: |- + CreatePodRange specifies whether to create a new range for + pod IPs in this node pool. + type: boolean + podRangeCidrBlock: + description: |- + PodRangeCidrBlock is the IP address range for pod IPs in + this node pool. + type: string + podRangeName: + description: PodRangeName is ID of the secondary range + for pod IPs. + type: string + tags: + description: |- + Tags is list of instance tags applied to all nodes. Tags + are used to identify valid sources or targets for network + firewalls. + items: + type: string + type: array + type: object + nodePoolName: + description: |- + NodePoolName specifies the name of the GKE node pool corresponding to this MachinePool. If you don't specify a name + then a default name will be created based on the namespace and name of the managed machine pool. + type: string + nodeSecurity: + description: NodeSecurity specifies the node security options. + properties: + enableIntegrityMonitoring: + description: |- + EnableIntegrityMonitoring defines whether the instance has + integrity monitoring enabled. + type: boolean + enableSecureBoot: + description: |- + EnableSecureBoot defines whether the instance has Secure + Boot enabled. + type: boolean + sandboxType: + description: SandboxType is type of the sandbox to use + for the node. + type: string + serviceAccount: + description: |- + ServiceAccount specifies the identity details for node + pool. + properties: + email: + description: |- + Email is the Google Cloud Platform Service Account to be + used by the node VMs. + type: string + scopes: + description: |- + Scopes is a set of Google API scopes to be made available + on all of the node VMs under the "default" service account. + items: + type: string + type: array + type: object + type: object + scaling: + description: Scaling specifies scaling for the node pool + properties: + enableAutoscaling: + description: Is autoscaling enabled for this node pool. + If unspecified, the default value is true. + type: boolean + locationPolicy: + description: Location policy used when scaling up a nodepool. + enum: + - balanced + - any + type: string + maxCount: + description: MaxCount specifies the maximum number of + nodes in the node pool + format: int32 + type: integer + minCount: + description: MinCount specifies the minimum number of + nodes in the node pool + format: int32 + type: integer + 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 874c9179e..f28da985c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -12,6 +12,11 @@ resources: - bases/infrastructure.cluster.x-k8s.io_gcpmanagedclusters.yaml - bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanes.yaml - bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepools.yaml +- bases/infrastructure.cluster.x-k8s.io_gcpmanagedclustertemplates.yaml +- bases/infrastructure.cluster.x-k8s.io_gcpmanagedcontrolplanetemplates.yaml +- bases/infrastructure.cluster.x-k8s.io_gcpmanagedmachinepooltemplates.yaml +- bases/bootstrap.cluster.x-k8s.io_gkeconfigs.yaml +- bases/bootstrap.cluster.x-k8s.io_gkeconfigtemplates.yaml # +kubebuilder:scaffold:crdkustomizeresource diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3bf8b1345..8f3ff3044 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -39,11 +39,37 @@ rules: - subjectaccessreviews verbs: - create +- apiGroups: + - bootstrap.cluster.x-k8s.io + resources: + - gkeconfigs + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - bootstrap.cluster.x-k8s.io + resources: + - gkeconfigs/status + verbs: + - get + - patch + - update - apiGroups: - cluster.x-k8s.io resources: - clusters - clusters/status + verbs: + - get + - list + - patch + - watch +- apiGroups: + - cluster.x-k8s.io + resources: - machinepools - machinepools/status - machines diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 0956ab370..038406b07 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -128,6 +128,27 @@ webhooks: resources: - gcpmanagedcontrolplanes sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedcontrolplanetemplate + failurePolicy: Fail + matchPolicy: Equivalent + name: mgcpmanagedcontrolplanetemplate.kb.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - gcpmanagedcontrolplanetemplates + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -148,6 +169,26 @@ webhooks: resources: - gcpmanagedmachinepools sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedmachinepooltemplate + failurePolicy: Fail + name: mgcpmanagedmachinepooltemplate.kb.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - gcpmanagedmachinepooltemplates + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -258,6 +299,25 @@ webhooks: resources: - gcpmanagedclusters sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedclustertemplate + failurePolicy: Fail + name: vgcpmanagedclustertemplate.kb.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta1 + operations: + - UPDATE + resources: + - gcpmanagedclustertemplates + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -278,6 +338,27 @@ webhooks: resources: - gcpmanagedcontrolplanes sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedcontrolplanetemplate + failurePolicy: Fail + matchPolicy: Equivalent + name: vgcpmanagedcontrolplanetemplate.kb.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - gcpmanagedcontrolplanetemplates + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/docs/book/src/developers/e2e.md b/docs/book/src/developers/e2e.md index 6b3003528..7b0ca4b65 100644 --- a/docs/book/src/developers/e2e.md +++ b/docs/book/src/developers/e2e.md @@ -32,7 +32,7 @@ Context("Creating a control-plane cluster with an internal load balancer", func( Flavor: "ci-with-internal-lb", Namespace: namespace.Name, ClusterName: clusterName, - KubernetesVersion: e2eConfig.GetVariable(KubernetesVersion), + KubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), ControlPlaneMachineCount: ptr.To[int64](1), WorkerMachineCount: ptr.To[int64](1), }, diff --git a/exp/api/v1beta1/gcpmanagedcluster_conversion.go b/exp/api/v1beta1/gcpmanagedcluster_conversion.go new file mode 100644 index 000000000..6e93e0bca --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedcluster_conversion.go @@ -0,0 +1,23 @@ +/* +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 v1beta1 + +// Hub marks GCPManagedCluster as a conversion hub. +func (*GCPManagedCluster) Hub() {} + +// Hub marks GCPManagedClusterList as a conversion hub. +func (*GCPManagedClusterList) Hub() {} diff --git a/exp/api/v1beta1/gcpmanagedclustertemplate_types.go b/exp/api/v1beta1/gcpmanagedclustertemplate_types.go new file mode 100644 index 000000000..274a2eea2 --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedclustertemplate_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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GCPManagedClusterTemplateSpec defines the desired state of GCPManagedClusterTemplate. +type GCPManagedClusterTemplateSpec struct { + Template GCPManagedClusterTemplateResource `json:"template"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=gcpmanagedclustertemplates,scope=Namespaced,categories=cluster-api,shortName=amct +// +kubebuilder:storageversion + +// GCPManagedClusterTemplate is the Schema for the GCPManagedClusterTemplates API. +type GCPManagedClusterTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GCPManagedClusterTemplateSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// GCPManagedClusterTemplateList contains a list of GCPManagedClusterTemplates. +type GCPManagedClusterTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GCPManagedClusterTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GCPManagedClusterTemplate{}, &GCPManagedClusterTemplateList{}) +} + +// GCPManagedClusterTemplateResource describes the data needed to create an GCPManagedCluster from a template. +type GCPManagedClusterTemplateResource struct { + Spec GCPManagedClusterTemplateResourceSpec `json:"spec"` +} diff --git a/exp/api/v1beta1/gcpmanagedclustertemplate_webhook.go b/exp/api/v1beta1/gcpmanagedclustertemplate_webhook.go new file mode 100644 index 000000000..57b6222f5 --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedclustertemplate_webhook.go @@ -0,0 +1,93 @@ +/* +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 v1beta1 + +import ( + "context" + "fmt" + "reflect" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var gmctlog = logf.Log.WithName("gcpclustertemplate-resource") + +// SetupWebhookWithManager sets up and registers the webhook with the manager. +func (r *GCPManagedClusterTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { + mctw := new(gcpManagedClusterTemplateWebhook) + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithValidator(mctw). + Complete() +} + +type gcpManagedClusterTemplateWebhook struct{} + +//+kubebuilder:webhook:verbs=update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedclustertemplate,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedclustertemplates,versions=v1beta1,name=vgcpmanagedclustertemplate.kb.io,admissionReviewVersions=v1 + +var _ webhook.CustomValidator = &gcpManagedClusterTemplateWebhook{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedClusterTemplateWebhook) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*GCPManagedClusterTemplate) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a GCPManagedClusterTemplate but got a %T", obj)) + } + + gmctlog.Info("Validating GCPManagedClusterTemplate create", "name", r.Name) + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedClusterTemplateWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + old, ok := oldObj.(*GCPManagedClusterTemplate) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a GCPManagedClusterTemplate but got a %T", oldObj)) + } + + r, ok := newObj.(*GCPManagedClusterTemplate) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a GCPManagedClusterTemplate but got a %T", newObj)) + } + + gmctlog.Info("Validating GCPManagedClusterTemplate update", "name", r.Name) + + if !reflect.DeepEqual(r.Spec, old.Spec) { + return nil, apierrors.NewBadRequest("GCPManagedClusterTemplate.Spec is immutable") + } + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedClusterTemplateWebhook) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*GCPManagedClusterTemplate) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a GCPManagedClusterTemplate but got a %T", obj)) + } + + gmctlog.Info("Validint GCPManagedClusterTemplate delete", "name", r.Name) + + return nil, nil +} diff --git a/exp/api/v1beta1/gcpmanagedcontrolplane_conversion.go b/exp/api/v1beta1/gcpmanagedcontrolplane_conversion.go new file mode 100644 index 000000000..a5a52238e --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedcontrolplane_conversion.go @@ -0,0 +1,23 @@ +/* +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 v1beta1 + +// Hub marks GCPManagedCluster as a conversion hub. +func (*GCPManagedControlPlane) Hub() {} + +// Hub marks GCPManagedClusterList as a conversion hub. +func (*GCPManagedControlPlaneList) Hub() {} diff --git a/exp/api/v1beta1/gcpmanagedcontrolplane_types.go b/exp/api/v1beta1/gcpmanagedcontrolplane_types.go index 53ecffcdd..6022836e0 100644 --- a/exp/api/v1beta1/gcpmanagedcontrolplane_types.go +++ b/exp/api/v1beta1/gcpmanagedcontrolplane_types.go @@ -115,6 +115,8 @@ type AuthenticatorGroupConfig struct { // GCPManagedControlPlaneSpec defines the desired state of GCPManagedControlPlane. type GCPManagedControlPlaneSpec struct { + GCPManagedControlPlaneClassSpec `json:",inline"` + // ClusterName allows you to specify the name of the GKE cluster. // 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. @@ -125,24 +127,6 @@ type GCPManagedControlPlaneSpec struct { // +optional Description string `json:"description,omitempty"` - // ClusterNetwork define the cluster network. - // +optional - ClusterNetwork *ClusterNetwork `json:"clusterNetwork,omitempty"` - - // Project is the name of the project to deploy the cluster to. - Project string `json:"project"` - // Location represents the location (region or zone) in which the GKE cluster - // will be created. - Location string `json:"location"` - // EnableAutopilot indicates whether to enable autopilot for this GKE cluster. - // +optional - EnableAutopilot bool `json:"enableAutopilot"` - // EnableIdentityService indicates whether to enable Identity Service component for this GKE cluster. - // +optional - EnableIdentityService bool `json:"enableIdentityService"` - // ReleaseChannel represents the release channel of the GKE cluster. - // +optional - ReleaseChannel *ReleaseChannel `json:"releaseChannel,omitempty"` // ControlPlaneVersion represents the control plane version of the GKE cluster. // If not specified, the default version currently supported by GKE will be // used. @@ -151,28 +135,16 @@ type GCPManagedControlPlaneSpec struct { // // +optional ControlPlaneVersion *string `json:"controlPlaneVersion,omitempty"` + // Version represents the control plane version of the GKE cluster. // If not specified, the default version currently supported by GKE will be // used. // +optional Version *string `json:"version,omitempty"` + // Endpoint represents the endpoint used to communicate with the control plane. // +optional Endpoint clusterv1.APIEndpoint `json:"endpoint"` - // MasterAuthorizedNetworksConfig represents configuration options for master authorized networks feature of the GKE cluster. - // This feature is disabled if this field is not specified. - // +optional - MasterAuthorizedNetworksConfig *MasterAuthorizedNetworksConfig `json:"master_authorized_networks_config,omitempty"` - // LoggingService represents configuration of logging service feature of the GKE cluster. - // Possible values: none, logging.googleapis.com/kubernetes (default). - // Value is ignored when enableAutopilot = true. - // +optional - LoggingService *LoggingService `json:"loggingService,omitempty"` - // MonitoringService represents configuration of monitoring service feature of the GKE cluster. - // Possible values: none, monitoring.googleapis.com/kubernetes (default). - // Value is ignored when enableAutopilot = true. - // +optional - MonitoringService *MonitoringService `json:"monitoringService,omitempty"` } // GCPManagedControlPlaneStatus defines the observed state of GCPManagedControlPlane. diff --git a/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go b/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go index 1daa2f2dc..d712f08c4 100644 --- a/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go +++ b/exp/api/v1beta1/gcpmanagedcontrolplane_webhook_test.go @@ -26,7 +26,7 @@ import ( ) var ( - vV1_27_1 = "v1.27.1" + vV1_32_5 = "v1.32.5" releaseChannel = Rapid ) @@ -74,20 +74,28 @@ func TestGCPManagedControlPlaneDefaultingWebhook(t *testing.T) { resourceNS: "default", spec: GCPManagedControlPlaneSpec{ ClusterName: "cluster1_27_1", - Version: &vV1_27_1, + Version: &vV1_32_5, }, - expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", Version: &vV1_27_1}, + expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_27_1", Version: &vV1_32_5}, }, { name: "with autopilot enabled", resourceName: "cluster1", resourceNS: "default", spec: GCPManagedControlPlaneSpec{ - ClusterName: "cluster1_autopilot", - Version: &vV1_27_1, - EnableAutopilot: true, + ClusterName: "cluster1_autopilot", + Version: &vV1_32_5, + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + EnableAutopilot: true, + }, + }, + expectSpec: GCPManagedControlPlaneSpec{ + ClusterName: "cluster1_autopilot", + Version: &vV1_32_5, + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + EnableAutopilot: true, + }, }, - expectSpec: GCPManagedControlPlaneSpec{ClusterName: "cluster1_autopilot", Version: &vV1_27_1, EnableAutopilot: true}, }, } @@ -138,9 +146,11 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) { expectError: true, expectWarn: false, spec: GCPManagedControlPlaneSpec{ - ClusterName: "", - EnableAutopilot: true, - ReleaseChannel: nil, + ClusterName: "", + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + EnableAutopilot: true, + ReleaseChannel: nil, + }, }, }, { @@ -148,9 +158,11 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) { expectError: false, expectWarn: false, spec: GCPManagedControlPlaneSpec{ - ClusterName: "", - EnableAutopilot: true, - ReleaseChannel: &releaseChannel, + ClusterName: "", + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + EnableAutopilot: true, + ReleaseChannel: &releaseChannel, + }, }, }, { @@ -159,7 +171,7 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) { expectWarn: true, spec: GCPManagedControlPlaneSpec{ ClusterName: "", - ControlPlaneVersion: &vV1_27_1, + ControlPlaneVersion: &vV1_32_5, }, }, { @@ -168,8 +180,8 @@ func TestGCPManagedControlPlaneValidatingWebhookCreate(t *testing.T) { expectWarn: false, spec: GCPManagedControlPlaneSpec{ ClusterName: "", - ControlPlaneVersion: &vV1_27_1, - Version: &vV1_27_1, + ControlPlaneVersion: &vV1_32_5, + Version: &vV1_32_5, }, }, } @@ -215,7 +227,9 @@ func TestGCPManagedControlPlaneValidatingWebhookUpdate(t *testing.T) { expectError: true, spec: GCPManagedControlPlaneSpec{ ClusterName: "default_cluster1", - Project: "new-project", + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + Project: "new-project", + }, }, }, { @@ -223,15 +237,19 @@ func TestGCPManagedControlPlaneValidatingWebhookUpdate(t *testing.T) { expectError: true, spec: GCPManagedControlPlaneSpec{ ClusterName: "default_cluster1", - Location: "us-west4", + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + Location: "us-west4", + }, }, }, { name: "request to enable/disable autopilot should cause an error", expectError: true, spec: GCPManagedControlPlaneSpec{ - ClusterName: "default_cluster1", - EnableAutopilot: true, + ClusterName: "default_cluster1", + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + EnableAutopilot: true, + }, }, }, { @@ -239,9 +257,11 @@ func TestGCPManagedControlPlaneValidatingWebhookUpdate(t *testing.T) { expectError: false, spec: GCPManagedControlPlaneSpec{ ClusterName: "default_cluster1", - ClusterNetwork: &ClusterNetwork{ - PrivateCluster: &PrivateCluster{ - EnablePrivateEndpoint: false, + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + ClusterNetwork: &ClusterNetwork{ + PrivateCluster: &PrivateCluster{ + EnablePrivateEndpoint: false, + }, }, }, }, @@ -258,9 +278,11 @@ func TestGCPManagedControlPlaneValidatingWebhookUpdate(t *testing.T) { oldMCP := &GCPManagedControlPlane{ Spec: GCPManagedControlPlaneSpec{ ClusterName: "default_cluster1", - ClusterNetwork: &ClusterNetwork{ - PrivateCluster: &PrivateCluster{ - EnablePrivateEndpoint: true, + GCPManagedControlPlaneClassSpec: GCPManagedControlPlaneClassSpec{ + ClusterNetwork: &ClusterNetwork{ + PrivateCluster: &PrivateCluster{ + EnablePrivateEndpoint: true, + }, }, }, }, diff --git a/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_types.go b/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_types.go new file mode 100644 index 000000000..de2ea2872 --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GCPManagedControlPlaneTemplateSpec defines the desired state of GCPManagedControlPlaneTemplate. +type GCPManagedControlPlaneTemplateSpec struct { + Template GCPManagedControlPlaneTemplateResource `json:"template"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=gcpmanagedcontrolplanetemplates,scope=Namespaced,categories=cluster-api,shortName=amcpt +// +kubebuilder:storageversion + +// GCPManagedControlPlaneTemplate is the Schema for the GCPManagedControlPlaneTemplates API. +type GCPManagedControlPlaneTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GCPManagedControlPlaneTemplateSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// GCPManagedControlPlaneTemplateList contains a list of GCPManagedControlPlaneTemplates. +type GCPManagedControlPlaneTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GCPManagedControlPlaneTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GCPManagedControlPlaneTemplate{}, &GCPManagedControlPlaneTemplateList{}) +} + +// GCPManagedControlPlaneTemplateResource describes the data needed to create an GCPManagedCluster from a template. +type GCPManagedControlPlaneTemplateResource struct { + Spec GCPManagedControlPlaneTemplateResourceSpec `json:"spec"` +} diff --git a/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_webhook.go b/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_webhook.go new file mode 100644 index 000000000..c7e3f9f5c --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_webhook.go @@ -0,0 +1,182 @@ +/* +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 v1beta1 + +import ( + "context" + "fmt" + + "github.com/google/go-cmp/cmp" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var gmcptlog = logf.Log.WithName("gcpmanagedcontrolplane-resource") + +func (r *GCPManagedControlPlaneTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { + mcptw := &gcpManagedControlPlaneTemplateWebhook{Client: mgr.GetClient()} + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithDefaulter(mcptw). + WithValidator(mcptw). + Complete() +} + +type gcpManagedControlPlaneTemplateWebhook struct { + Client client.Client +} + +//+kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedcontrolplanetemplate,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedcontrolplanetemplates,versions=v1beta1,name=vgcpmanagedcontrolplanetemplate.kb.io,sideEffects=None,admissionReviewVersions=v1 +//+kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedcontrolplanetemplate,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedcontrolplanetemplates,versions=v1beta1,name=mgcpmanagedcontrolplanetemplate.kb.io,sideEffects=None,admissionReviewVersions=v1 + +var ( + _ webhook.CustomValidator = &gcpManagedControlPlaneTemplateWebhook{} + _ webhook.CustomDefaulter = &gcpManagedControlPlaneTemplateWebhook{} +) + +// Default implements webhook.Defaulter so a webhook will be registered for the type. +func (mcpw *gcpManagedControlPlaneTemplateWebhook) Default(_ context.Context, obj runtime.Object) error { + r, ok := obj.(*GCPManagedControlPlaneTemplate) + if !ok { + return fmt.Errorf("expected a GCPManagedControlPlaneTemplate object but got %T", r) + } + + gcpmanagedcontrolplanelog.Info("default", "name", r.Name) + + return nil +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedControlPlaneTemplateWebhook) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*GCPManagedControlPlaneTemplate) + if !ok { + return nil, apierrors.NewBadRequest("expected a GCPManagedControlPlaneTemplate") + } + + gmcptlog.Info("Validate GCPManagedControlPlaneTemplate create", "name", r.Name) + + var allErrs field.ErrorList + var allWarns admission.Warnings + + if r.Spec.Template.Spec.EnableAutopilot && r.Spec.Template.Spec.ReleaseChannel == nil { + allErrs = append(allErrs, field.Required(field.NewPath("spec", "ReleaseChannel"), "Release channel is required for an autopilot enabled cluster")) + } + + if r.Spec.Template.Spec.EnableAutopilot && r.Spec.Template.Spec.LoggingService != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "LoggingService"), + r.Spec.Template.Spec.LoggingService, "can't be set when autopilot is enabled")) + } + + if r.Spec.Template.Spec.EnableAutopilot && r.Spec.Template.Spec.MonitoringService != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "MonitoringService"), + r.Spec.Template.Spec.LoggingService, "can't be set when autopilot is enabled")) + } + + if len(allErrs) == 0 { + return allWarns, nil + } + + return allWarns, apierrors.NewInvalid(GroupVersion.WithKind("GCPManagedControlPlaneTemplate").GroupKind(), r.Name, allErrs) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedControlPlaneTemplateWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + old, ok := oldObj.(*GCPManagedControlPlaneTemplate) + if !ok { + return nil, apierrors.NewBadRequest("expected a GCPManagedControlPlaneTemplate") + } + + r, ok := newObj.(*GCPManagedControlPlaneTemplate) + if !ok { + return nil, apierrors.NewBadRequest("expected a GCPManagedControlPlaneTemplate") + } + + gmcptlog.Info("Validating GCPManagedControlPlaneTemplate update", "name", r.Name) + + var allErrs field.ErrorList + if !cmp.Equal(r.Spec.Template.Spec.Project, old.Spec.Template.Spec.Project) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "Project"), + r.Spec.Template.Spec.Project, "field is immutable"), + ) + } + + if !cmp.Equal(r.Spec.Template.Spec.Location, old.Spec.Template.Spec.Location) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "Location"), + r.Spec.Template.Spec.Location, "field is immutable"), + ) + } + + if !cmp.Equal(r.Spec.Template.Spec.EnableAutopilot, old.Spec.Template.Spec.EnableAutopilot) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "EnableAutopilot"), + r.Spec.Template.Spec.EnableAutopilot, "field is immutable"), + ) + } + + if old.Spec.Template.Spec.EnableAutopilot && r.Spec.Template.Spec.LoggingService != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "LoggingService"), + r.Spec.Template.Spec.LoggingService, "can't be set when autopilot is enabled")) + } + + if old.Spec.Template.Spec.EnableAutopilot && r.Spec.Template.Spec.MonitoringService != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "MonitoringService"), + r.Spec.Template.Spec.LoggingService, "can't be set when autopilot is enabled")) + } + + if r.Spec.Template.Spec.LoggingService != nil { + err := r.Spec.Template.Spec.LoggingService.Validate() + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "LoggingService"), + r.Spec.Template.Spec.LoggingService, err.Error())) + } + } + + if r.Spec.Template.Spec.MonitoringService != nil { + err := r.Spec.Template.Spec.MonitoringService.Validate() + if err != nil { + allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "MonitoringService"), + r.Spec.Template.Spec.MonitoringService, err.Error())) + } + } + + if len(allErrs) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid(GroupVersion.WithKind("GCPManagedControlPlaneTemplate").GroupKind(), r.Name, allErrs) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedControlPlaneTemplateWebhook) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*GCPManagedControlPlaneTemplate) + if !ok { + return nil, apierrors.NewBadRequest("expected a GCPManagedControlPlaneTemplate") + } + + gmcptlog.Info("Validating GCPManagedControlPlaneTemplate delete", "name", r.Name) + + return nil, nil +} diff --git a/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_webhook_test.go b/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_webhook_test.go new file mode 100644 index 000000000..b5520db3f --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedcontrolplanetemplate_webhook_test.go @@ -0,0 +1,17 @@ +/* +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 v1beta1 diff --git a/exp/api/v1beta1/gcpmanagedmachinepool_conversion.go b/exp/api/v1beta1/gcpmanagedmachinepool_conversion.go new file mode 100644 index 000000000..4fbc93f9d --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedmachinepool_conversion.go @@ -0,0 +1,23 @@ +/* +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 v1beta1 + +// Hub marks GCPManagedMachinePool as a conversion hub. +func (*GCPManagedMachinePool) Hub() {} + +// Hub marks GCPManagedMachinePoolList as a conversion hub. +func (*GCPManagedMachinePoolList) Hub() {} diff --git a/exp/api/v1beta1/gcpmanagedmachinepool_types.go b/exp/api/v1beta1/gcpmanagedmachinepool_types.go index 2420f5465..5d16e6b58 100644 --- a/exp/api/v1beta1/gcpmanagedmachinepool_types.go +++ b/exp/api/v1beta1/gcpmanagedmachinepool_types.go @@ -18,7 +18,6 @@ package v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - infrav1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ) @@ -26,6 +25,9 @@ const ( // ManagedMachinePoolFinalizer allows Reconcile to clean up GCP resources associated with the GCPManagedMachinePool before // removing it from the apiserver. ManagedMachinePoolFinalizer = "gcpmanagedmachinepool.infrastructure.cluster.x-k8s.io" + + // GCPManagedMachinePoolMachineKind indicates the kind of an GCPManagedMachinePoolMachine. + GCPManagedMachinePoolMachineKind = "GCPManagedMachinePool" ) // DiskType is type of the disk attached to node. @@ -45,72 +47,8 @@ const ( // GCPManagedMachinePoolSpec defines the desired state of GCPManagedMachinePool. type GCPManagedMachinePoolSpec struct { - // NodePoolName specifies the name of the GKE node pool corresponding to this MachinePool. If you don't specify a name - // then a default name will be created based on the namespace and name of the managed machine pool. - // +optional - NodePoolName string `json:"nodePoolName,omitempty"` - // MachineType is the name of a Google Compute Engine [machine - // type](https://cloud.google.com/compute/docs/machine-types). - // If unspecified, the default machine type is `e2-medium`. - // +optional - MachineType *string `json:"machineType,omitempty"` - // DiskSizeGb is the size of the disk attached to each node, specified in GB. - // The smallest allowed disk size is 10GB. If unspecified, the default disk size is 100GB. - // +optional - DiskSizeGb *int32 `json:"diskSizeGb,omitempty"` - // LocalSsdCount is the number of local SSD disks to be attached to the node. - // +optional - LocalSsdCount *int32 `json:"localSsdCount,omitempty"` - // Scaling specifies scaling for the node pool - // +optional - Scaling *NodePoolAutoScaling `json:"scaling,omitempty"` - // NodeLocations is the list of zones in which the NodePool's - // nodes should be located. - // +optional - NodeLocations []string `json:"nodeLocations,omitempty"` - // ImageType is image type to use for this nodepool. - // +optional - ImageType *string `json:"imageType,omitempty"` - // InstanceType is name of Compute Engine machine type. - // +optional - InstanceType *string `json:"instanceType,omitempty"` - // DiskType is type of the disk attached to each node. - // +optional - DiskType *DiskType `json:"diskType,omitempty"` - // DiskSizeGB is size of the disk attached to each node, - // specified in GB. - // +kubebuilder:validation:Minimum:=10 - // +optional - DiskSizeGB *int64 `json:"diskSizeGB,omitempty"` - // MaxPodsPerNode is constraint enforced on the max num of - // pods per node. - // +kubebuilder:validation:Minimum:=8 - // +kubebuilder:validation:Maximum:=256 - // +optional - MaxPodsPerNode *int64 `json:"maxPodsPerNode,omitempty"` - // NodeNetwork specifies the node network configuration - // options. - // +optional - NodeNetwork NodeNetworkConfig `json:"nodeNetwork,omitempty"` - // NodeSecurity specifies the node security options. - // +optional - NodeSecurity NodeSecurityConfig `json:"nodeSecurity,omitempty"` - // KubernetesLabels specifies the labels to apply to the nodes of the node pool. - // +optional - KubernetesLabels infrav1.Labels `json:"kubernetesLabels,omitempty"` - // KubernetesTaints specifies the taints to apply to the nodes of the node pool. - // +optional - KubernetesTaints Taints `json:"kubernetesTaints,omitempty"` - // AdditionalLabels is an optional set of tags to add to GCP resources managed by the GCP provider, in addition to the - // ones added by default. - // +optional - AdditionalLabels infrav1.Labels `json:"additionalLabels,omitempty"` - // Management specifies the node pool management options. - // +optional - Management *NodePoolManagement `json:"management,omitempty"` - // LinuxNodeConfig specifies the settings for Linux agent nodes. - // +optional - LinuxNodeConfig *LinuxNodeConfig `json:"linuxNodeConfig,omitempty"` + GCPManagedMachinePoolClassSpec `json:",inline"` + // ProviderIDList are the provider IDs of instances in the // managed instance group corresponding to the nodegroup represented by this // machine pool @@ -179,6 +117,9 @@ type GCPManagedMachinePoolStatus struct { Replicas int32 `json:"replicas"` // Conditions specifies the cpnditions for the managed machine pool Conditions clusterv1.Conditions `json:"conditions,omitempty"` + // InfrastructureMachineKind is the kind of the infrastructure resources behind MachinePool Machines. + // +optional + InfrastructureMachineKind string `json:"infrastructureMachineKind,omitempty"` } // +kubebuilder:object:root=true diff --git a/exp/api/v1beta1/gcpmanagedmachinepool_webhook.go b/exp/api/v1beta1/gcpmanagedmachinepool_webhook.go index f7182baee..a12ba94d1 100644 --- a/exp/api/v1beta1/gcpmanagedmachinepool_webhook.go +++ b/exp/api/v1beta1/gcpmanagedmachinepool_webhook.go @@ -20,10 +20,10 @@ import ( "context" "fmt" - "github.com/google/go-cmp/cmp" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" + webhookutils "sigs.k8s.io/cluster-api-provider-gcp/util/webhook" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -35,7 +35,7 @@ const ( ) // log is for logging in this package. -var gcpmanagedmachinepoollog = logf.Log.WithName("gcpmanagedmachinepool-resource") +var gcpmanagedmachinepoollog = logf.Log.WithName("gcpmanagedmachinepooltemplate-resource") func (r *GCPManagedMachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error { w := new(gcpManagedMachinePoolWebhook) @@ -61,116 +61,48 @@ func (*gcpManagedMachinePoolWebhook) Default(_ context.Context, _ runtime.Object var _ webhook.CustomValidator = &gcpManagedMachinePoolWebhook{} -// validateSpec validates that the GCPManagedMachinePool spec is valid. -func (r *GCPManagedMachinePool) validateSpec() field.ErrorList { - var allErrs field.ErrorList - - // Validate node pool name - if len(r.Spec.NodePoolName) > maxNodePoolNameLength { - allErrs = append(allErrs, - field.Invalid(field.NewPath("spec", "NodePoolName"), - r.Spec.NodePoolName, fmt.Sprintf("node pool name cannot have more than %d characters", maxNodePoolNameLength)), - ) - } - - if errs := r.validateScaling(); errs != nil || len(errs) == 0 { - allErrs = append(allErrs, errs...) - } - - if errs := r.validateNonNegative(); errs != nil || len(errs) == 0 { - allErrs = append(allErrs, errs...) +func validateNodePoolName(name string, fldPath *field.Path) *field.Error { + if len(name) > maxNodePoolNameLength { + return (field.Invalid( + fldPath, + name, + fmt.Sprintf("node pool name cannot have more than %d characters", maxNodePoolNameLength), + )) } - if len(allErrs) == 0 { - return nil - } - return allErrs + return nil } -// validateScaling validates that the GCPManagedMachinePool autoscaling spec is valid. -func (r *GCPManagedMachinePool) validateScaling() field.ErrorList { +func validateScaling(scaling *NodePoolAutoScaling, minField, maxField, locationPolicyField *field.Path) field.ErrorList { var allErrs field.ErrorList - if r.Spec.Scaling != nil { - minField := field.NewPath("spec", "scaling", "minCount") - maxField := field.NewPath("spec", "scaling", "maxCount") - locationPolicyField := field.NewPath("spec", "scaling", "locationPolicy") - - minCount := r.Spec.Scaling.MinCount - maxCount := r.Spec.Scaling.MaxCount - locationPolicy := r.Spec.Scaling.LocationPolicy - - // cannot specify autoscaling config if autoscaling is disabled - if r.Spec.Scaling.EnableAutoscaling != nil && !*r.Spec.Scaling.EnableAutoscaling { - if minCount != nil { - allErrs = append(allErrs, field.Forbidden(minField, "minCount cannot be specified when autoscaling is disabled")) - } - if maxCount != nil { - allErrs = append(allErrs, field.Forbidden(maxField, "maxCount cannot be specified when autoscaling is disabled")) - } - if locationPolicy != nil { - allErrs = append(allErrs, field.Forbidden(locationPolicyField, "locationPolicy cannot be specified when autoscaling is disabled")) - } - } - if minCount != nil { - // validates min >= 0 - if *minCount < 0 { - allErrs = append(allErrs, field.Invalid(minField, *minCount, "must be greater or equal zero")) - } - // validates min <= max - if maxCount != nil && *maxCount < *minCount { - allErrs = append(allErrs, field.Invalid(maxField, *maxCount, "must be greater than field "+minField.String())) - } + // cannot specify autoscaling config if autoscaling is disabled + if scaling.EnableAutoscaling != nil && !*scaling.EnableAutoscaling { + if scaling.MinCount != nil { + allErrs = append(allErrs, field.Forbidden(minField, "minCount cannot be specified when autoscaling is disabled")) + } + if scaling.MaxCount != nil { + allErrs = append(allErrs, field.Forbidden(maxField, "maxCount cannot be specified when autoscaling is disabled")) + } + if scaling.LocationPolicy != nil { + allErrs = append(allErrs, field.Forbidden(locationPolicyField, "locationPolicy cannot be specified when autoscaling is disabled")) } } - if len(allErrs) == 0 { - return nil - } - return allErrs -} -func appendErrorIfNegative[T int32 | int64](value *T, name string, errs *field.ErrorList) { - if value != nil && *value < 0 { - *errs = append(*errs, field.Invalid(field.NewPath("spec", name), *value, "must be non-negative")) + if scaling.MinCount != nil { + // validates min >= 0 + if *scaling.MinCount < 0 { + allErrs = append(allErrs, field.Invalid(minField, *scaling.MinCount, "must be greater or equal zero")) + } + // validates min <= max + if scaling.MaxCount != nil && *scaling.MaxCount < *scaling.MinCount { + allErrs = append(allErrs, field.Invalid(maxField, *scaling.MaxCount, "must be greater than field "+minField.String())) + } } -} - -// validateNonNegative validates that non-negative GCPManagedMachinePool spec fields are not negative. -func (r *GCPManagedMachinePool) validateNonNegative() field.ErrorList { - var allErrs field.ErrorList - - appendErrorIfNegative(r.Spec.DiskSizeGb, "diskSizeGb", &allErrs) - appendErrorIfNegative(r.Spec.MaxPodsPerNode, "maxPodsPerNode", &allErrs) - appendErrorIfNegative(r.Spec.LocalSsdCount, "localSsdCount", &allErrs) - - return allErrs -} -func appendErrorIfMutated(old, update interface{}, name string, errs *field.ErrorList) { - if !cmp.Equal(old, update) { - *errs = append( - *errs, - field.Invalid(field.NewPath("spec", name), update, "field is immutable"), - ) + if len(allErrs) == 0 { + return nil } -} - -// validateImmutable validates that immutable GCPManagedMachinePool spec fields are not mutated. -func (r *GCPManagedMachinePool) validateImmutable(old *GCPManagedMachinePool) field.ErrorList { - var allErrs field.ErrorList - - appendErrorIfMutated(old.Spec.InstanceType, r.Spec.InstanceType, "instanceType", &allErrs) - appendErrorIfMutated(old.Spec.NodePoolName, r.Spec.NodePoolName, "nodePoolName", &allErrs) - appendErrorIfMutated(old.Spec.MachineType, r.Spec.MachineType, "machineType", &allErrs) - appendErrorIfMutated(old.Spec.DiskSizeGb, r.Spec.DiskSizeGb, "diskSizeGb", &allErrs) - appendErrorIfMutated(old.Spec.DiskType, r.Spec.DiskType, "diskType", &allErrs) - appendErrorIfMutated(old.Spec.LocalSsdCount, r.Spec.LocalSsdCount, "localSsdCount", &allErrs) - appendErrorIfMutated(old.Spec.Management, r.Spec.Management, "management", &allErrs) - appendErrorIfMutated(old.Spec.MaxPodsPerNode, r.Spec.MaxPodsPerNode, "maxPodsPerNode", &allErrs) - appendErrorIfMutated(old.Spec.NodeNetwork.PodRangeName, r.Spec.NodeNetwork.PodRangeName, "podRangeName", &allErrs) - appendErrorIfMutated(old.Spec.NodeNetwork.CreatePodRange, r.Spec.NodeNetwork.CreatePodRange, "createPodRange", &allErrs) - appendErrorIfMutated(old.Spec.NodeNetwork.PodRangeCidrBlock, r.Spec.NodeNetwork.PodRangeCidrBlock, "podRangeCidrBlock", &allErrs) - appendErrorIfMutated(old.Spec.NodeSecurity, r.Spec.NodeSecurity, "nodeSecurity", &allErrs) return allErrs } @@ -182,37 +114,168 @@ func (*gcpManagedMachinePoolWebhook) ValidateCreate(_ context.Context, obj runti return nil, fmt.Errorf("expected an GCPManagedMachinePool object but got %T", r) } - gcpmanagedmachinepoollog.Info("validate create", "name", r.Name) + gcpmanagedmachinepoollog.Info("Validating GCPManagedMachinePool create", "name", r.Name) + var allErrs field.ErrorList - if errs := r.validateSpec(); errs != nil || len(errs) == 0 { - allErrs = append(allErrs, errs...) + if err := validateNodePoolName( + r.Spec.NodePoolName, + field.NewPath("spec", "NodePoolName")); err != nil { + allErrs = append(allErrs, err) + } + + if r.Spec.Scaling != nil { + if errs := validateScaling( + r.Spec.Scaling, + field.NewPath("spec", "scaling", "minCount"), + field.NewPath("spec", "scaling", "maxCount"), + field.NewPath("spec", "scaling", "locationPolicy"), + ); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + } + + if err := webhookutils.ValidateNonNegative( + field.NewPath("spec", "template", "spec", "diskSizeGb"), + r.Spec.DiskSizeGb, + ); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateNonNegative( + field.NewPath("spec", "template", "spec", "localSsdCount"), + r.Spec.LocalSsdCount, + ); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateNonNegative( + field.NewPath("spec", "template", "spec", "maxPodsPerNode"), + r.Spec.MaxPodsPerNode, + ); err != nil { + allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil, nil } - return nil, apierrors.NewInvalid(GroupVersion.WithKind("GCPManagedMachinePool").GroupKind(), r.Name, allErrs) + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. func (*gcpManagedMachinePoolWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + old, ok := oldObj.(*GCPManagedMachinePool) + if !ok { + return nil, fmt.Errorf("expected an GCPManagedMachinePool object but got %T", old) + } + r, ok := newObj.(*GCPManagedMachinePool) if !ok { return nil, fmt.Errorf("expected an GCPManagedMachinePool object but got %T", r) } - gcpmanagedmachinepoollog.Info("validate update", "name", r.Name) + gcpmanagedmachinepoollog.Info("Validating GCPManagedMachinePool update", "name", r.Name) + var allErrs field.ErrorList - old := oldObj.(*GCPManagedMachinePool) - if errs := r.validateImmutable(old); errs != nil { - allErrs = append(allErrs, errs...) + if r.Spec.Scaling != nil { + if errs := validateScaling( + r.Spec.Scaling, + field.NewPath("spec", "scaling", "minCount"), + field.NewPath("spec", "scaling", "maxCount"), + field.NewPath("spec", "scaling", "locationPolicy"), + ); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "instanceType"), + old.Spec.InstanceType, + r.Spec.InstanceType); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "nodePoolName"), + old.Spec.NodePoolName, + r.Spec.NodePoolName); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "machineType"), + old.Spec.MachineType, + r.Spec.MachineType); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "diskSizeGb"), + old.Spec.DiskSizeGb, + r.Spec.DiskSizeGb); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "diskType"), + old.Spec.DiskType, + r.Spec.DiskType); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "localSsdCount"), + old.Spec.LocalSsdCount, + r.Spec.LocalSsdCount); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "management"), + old.Spec.Management, + r.Spec.Management); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "maxPodsPerNode"), + old.Spec.MaxPodsPerNode, + r.Spec.MaxPodsPerNode); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "nodeNetwork", "podRangeName"), + old.Spec.NodeNetwork.PodRangeName, + r.Spec.NodeNetwork.PodRangeName); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "nodeNetwork", "createPodRange"), + old.Spec.NodeNetwork.CreatePodRange, + r.Spec.NodeNetwork.CreatePodRange); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "nodeNetwork", "podRangeCidrBlock"), + old.Spec.NodeNetwork.PodRangeCidrBlock, + r.Spec.NodeNetwork.PodRangeCidrBlock); err != nil { + allErrs = append(allErrs, err) } - if errs := r.validateSpec(); errs != nil || len(errs) == 0 { - allErrs = append(allErrs, errs...) + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "nodeSecurity"), + old.Spec.NodeSecurity, + r.Spec.NodeSecurity); err != nil { + allErrs = append(allErrs, err) } if len(allErrs) == 0 { diff --git a/exp/api/v1beta1/gcpmanagedmachinepool_webhook_test.go b/exp/api/v1beta1/gcpmanagedmachinepool_webhook_test.go index c2e66f00b..1334c5de2 100644 --- a/exp/api/v1beta1/gcpmanagedmachinepool_webhook_test.go +++ b/exp/api/v1beta1/gcpmanagedmachinepool_webhook_test.go @@ -47,24 +47,30 @@ func TestGCPManagedMachinePoolValidatingWebhookCreate(t *testing.T) { { name: "valid node pool name", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + }, }, expectError: false, }, { name: "node pool name is too long", spec: GCPManagedMachinePoolSpec{ - NodePoolName: strings.Repeat("A", maxNodePoolNameLength+1), + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: strings.Repeat("A", maxNodePoolNameLength+1), + }, }, expectError: true, }, { name: "scaling with valid min/max count", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - Scaling: &NodePoolAutoScaling{ - MinCount: &minCount, - MaxCount: &maxCount, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + Scaling: &NodePoolAutoScaling{ + MinCount: &minCount, + MaxCount: &maxCount, + }, }, }, expectError: false, @@ -72,10 +78,12 @@ func TestGCPManagedMachinePoolValidatingWebhookCreate(t *testing.T) { { name: "scaling with invalid min/max count", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - Scaling: &NodePoolAutoScaling{ - MinCount: &invalidMinCount, - MaxCount: &maxCount, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + Scaling: &NodePoolAutoScaling{ + MinCount: &invalidMinCount, + MaxCount: &maxCount, + }, }, }, expectError: true, @@ -83,10 +91,12 @@ func TestGCPManagedMachinePoolValidatingWebhookCreate(t *testing.T) { { name: "scaling with max < min count", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - Scaling: &NodePoolAutoScaling{ - MinCount: &maxCount, - MaxCount: &minCount, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + Scaling: &NodePoolAutoScaling{ + MinCount: &maxCount, + MaxCount: &minCount, + }, }, }, expectError: true, @@ -94,11 +104,13 @@ func TestGCPManagedMachinePoolValidatingWebhookCreate(t *testing.T) { { name: "autoscaling disabled and min/max provided", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - Scaling: &NodePoolAutoScaling{ - EnableAutoscaling: &enableAutoscaling, - MinCount: &minCount, - MaxCount: &maxCount, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + Scaling: &NodePoolAutoScaling{ + EnableAutoscaling: &enableAutoscaling, + MinCount: &minCount, + MaxCount: &maxCount, + }, }, }, expectError: true, @@ -106,20 +118,24 @@ func TestGCPManagedMachinePoolValidatingWebhookCreate(t *testing.T) { { name: "valid non-negative values", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - DiskSizeGb: &diskSizeGb, - MaxPodsPerNode: &maxPods, - LocalSsdCount: &localSsds, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + DiskSizeGb: &diskSizeGb, + MaxPodsPerNode: &maxPods, + LocalSsdCount: &localSsds, + }, }, expectError: false, }, { name: "invalid negative values", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - DiskSizeGb: &invalidDiskSizeGb, - MaxPodsPerNode: &invalidMaxPods, - LocalSsdCount: &invalidLocalSsds, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + DiskSizeGb: &invalidDiskSizeGb, + MaxPodsPerNode: &invalidMaxPods, + LocalSsdCount: &invalidLocalSsds, + }, }, expectError: true, }, @@ -154,16 +170,20 @@ func TestGCPManagedMachinePoolValidatingWebhookUpdate(t *testing.T) { { name: "node pool is not mutated", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + }, }, expectError: false, }, { name: "mutable fields are mutated", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - AdditionalLabels: infrav1.Labels{ - "testKey": "testVal", + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + AdditionalLabels: infrav1.Labels{ + "testKey": "testVal", + }, }, }, expectError: false, @@ -171,8 +191,10 @@ func TestGCPManagedMachinePoolValidatingWebhookUpdate(t *testing.T) { { name: "immutable field disk size is mutated", spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", - DiskSizeGb: &diskSizeGb, + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + DiskSizeGb: &diskSizeGb, + }, }, expectError: true, }, @@ -187,7 +209,9 @@ func TestGCPManagedMachinePoolValidatingWebhookUpdate(t *testing.T) { } oldMMP := &GCPManagedMachinePool{ Spec: GCPManagedMachinePoolSpec{ - NodePoolName: "nodepool1", + GCPManagedMachinePoolClassSpec: GCPManagedMachinePoolClassSpec{ + NodePoolName: "nodepool1", + }, }, } diff --git a/exp/api/v1beta1/gcpmanagedmachinepooltemplate_types.go b/exp/api/v1beta1/gcpmanagedmachinepooltemplate_types.go new file mode 100644 index 000000000..5e85e8ec1 --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedmachinepooltemplate_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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GCPManagedMachinePoolTemplateSpec defines the desired state of GCPManagedMachinePoolTemplate. +type GCPManagedMachinePoolTemplateSpec struct { + Template GCPManagedMachinePoolTemplateResource `json:"template"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=gcpmanagedmachinepooltemplates,scope=Namespaced,categories=cluster-api,shortName=ammpt +// +kubebuilder:storageversion + +// GCPManagedMachinePoolTemplate is the Schema for the GCPManagedMachinePoolTemplates API. +type GCPManagedMachinePoolTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GCPManagedMachinePoolTemplateSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// GCPManagedMachinePoolTemplateList contains a list of GCPManagedMachinePoolTemplates. +type GCPManagedMachinePoolTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GCPManagedMachinePoolTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GCPManagedMachinePoolTemplate{}, &GCPManagedMachinePoolTemplateList{}) +} + +// GCPManagedMachinePoolTemplateResource describes the data needed to create an GCPManagedCluster from a template. +type GCPManagedMachinePoolTemplateResource struct { + Spec GCPManagedMachinePoolTemplateResourceSpec `json:"spec"` +} diff --git a/exp/api/v1beta1/gcpmanagedmachinepooltemplate_webhook.go b/exp/api/v1beta1/gcpmanagedmachinepooltemplate_webhook.go new file mode 100644 index 000000000..84d1cdff6 --- /dev/null +++ b/exp/api/v1beta1/gcpmanagedmachinepooltemplate_webhook.go @@ -0,0 +1,248 @@ +/* +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 v1beta1 + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + webhookutils "sigs.k8s.io/cluster-api-provider-gcp/util/webhook" +) + +// log is for logging in this package. +var gmmplog = logf.Log.WithName("gcpmanagedmachinepool-resource") + +func (r *GCPManagedMachinePoolTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { + mmptw := &gcpManagedMachinePoolTemplateWebhook{Client: mgr.GetClient()} + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithDefaulter(mmptw). + WithValidator(mmptw). + Complete() +} + +type gcpManagedMachinePoolTemplateWebhook struct { + Client client.Client +} + +//+kubebuilder:webhook:verbs=create;update,path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-gcpmanagedmachinepooltemplate,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedmachinepooltemplates,versions=v1beta1,name=mgcpmanagedmachinepooltemplate.kb.io,admissionReviewVersions=v1 + +// Default implements webhook.Defaulter so a webhook will be registered for the type. +func (*gcpManagedMachinePoolTemplateWebhook) Default(_ context.Context, obj runtime.Object) error { + r, ok := obj.(*GCPManagedMachinePoolTemplate) + if !ok { + return fmt.Errorf("expected a GCPManagedMachinePoolTemplate object but got %T", r) + } + + gmmplog.Info("default", "name", r.Name) + + return nil +} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedMachinePoolTemplateWebhook) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + r, ok := obj.(*GCPManagedMachinePoolTemplate) + if !ok { + return nil, fmt.Errorf("expected an GCPManagedMachinePoolTemplate object but got %T", r) + } + + gmmplog.Info("Validating GCPManagedMachinePoolTemplate create", "name", r.Name) + + var allErrs field.ErrorList + + if err := validateNodePoolName( + r.Spec.Template.Spec.NodePoolName, + field.NewPath("spec", "NodePoolName")); err != nil { + allErrs = append(allErrs, err) + } + + if r.Spec.Template.Spec.Scaling != nil { + if errs := validateScaling( + r.Spec.Template.Spec.Scaling, + field.NewPath("spec", "scaling", "minCount"), + field.NewPath("spec", "scaling", "maxCount"), + field.NewPath("spec", "scaling", "locationPolicy"), + ); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + } + + if err := webhookutils.ValidateNonNegative( + field.NewPath("spec", "template", "spec", "diskSizeGb"), + r.Spec.Template.Spec.DiskSizeGb, + ); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateNonNegative( + field.NewPath("spec", "template", "spec", "localSsdCount"), + r.Spec.Template.Spec.LocalSsdCount, + ); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateNonNegative( + field.NewPath("spec", "template", "spec", "maxPodsPerNode"), + r.Spec.Template.Spec.MaxPodsPerNode, + ); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil, nil + } + + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedMachinePoolTemplateWebhook) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + old, ok := oldObj.(*GCPManagedMachinePoolTemplate) + if !ok { + return nil, fmt.Errorf("expected a GCPManagedMachinePoolTemplate object but got %T", old) + } + + r, ok := newObj.(*GCPManagedMachinePoolTemplate) + if !ok { + return nil, fmt.Errorf("expected a GCPManagedMachinePoolTemplate object but got %T", r) + } + + gcpmanagedmachinepoollog.Info("Validating GCPManagedMachinePoolTemplate update", "name", r.Name) + + var allErrs field.ErrorList + + if r.Spec.Template.Spec.Scaling != nil { + if errs := validateScaling( + r.Spec.Template.Spec.Scaling, + field.NewPath("spec", "scaling", "minCount"), + field.NewPath("spec", "scaling", "maxCount"), + field.NewPath("spec", "scaling", "locationPolicy"), + ); len(errs) > 0 { + allErrs = append(allErrs, errs...) + } + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "instanceType"), + old.Spec.Template.Spec.InstanceType, + r.Spec.Template.Spec.InstanceType); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "nodePoolName"), + old.Spec.Template.Spec.NodePoolName, + r.Spec.Template.Spec.NodePoolName); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "machineType"), + old.Spec.Template.Spec.MachineType, + r.Spec.Template.Spec.MachineType); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "diskSizeGb"), + old.Spec.Template.Spec.DiskSizeGb, + r.Spec.Template.Spec.DiskSizeGb); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "diskType"), + old.Spec.Template.Spec.DiskType, + r.Spec.Template.Spec.DiskType); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "localSsdCount"), + old.Spec.Template.Spec.LocalSsdCount, + r.Spec.Template.Spec.LocalSsdCount); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "management"), + old.Spec.Template.Spec.Management, + r.Spec.Template.Spec.Management); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "maxPodsPerNode"), + old.Spec.Template.Spec.MaxPodsPerNode, + r.Spec.Template.Spec.MaxPodsPerNode); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "nodeNetwork", "podRangeName"), + old.Spec.Template.Spec.NodeNetwork.PodRangeName, + r.Spec.Template.Spec.NodeNetwork.PodRangeName); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "nodeNetwork", "createPodRange"), + old.Spec.Template.Spec.NodeNetwork.CreatePodRange, + r.Spec.Template.Spec.NodeNetwork.CreatePodRange); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "nodeNetwork", "podRangeCidrBlock"), + old.Spec.Template.Spec.NodeNetwork.PodRangeCidrBlock, + r.Spec.Template.Spec.NodeNetwork.PodRangeCidrBlock); err != nil { + allErrs = append(allErrs, err) + } + + if err := webhookutils.ValidateImmutable( + field.NewPath("spec", "template", "spec", "nodeSecurity"), + old.Spec.Template.Spec.NodeSecurity, + r.Spec.Template.Spec.NodeSecurity); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil, nil + } + return nil, apierrors.NewInvalid( + r.GroupVersionKind().GroupKind(), + r.Name, + allErrs, + ) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type. +func (*gcpManagedMachinePoolTemplateWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/exp/api/v1beta1/types_class.go b/exp/api/v1beta1/types_class.go new file mode 100644 index 000000000..5027ddf14 --- /dev/null +++ b/exp/api/v1beta1/types_class.go @@ -0,0 +1,139 @@ +/* +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 v1beta1 + +import infrav1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" + +// GCPManagedControlPlaneClassSpec defines the GCPManagedControlPlane properties that may be shared across several gcp managed control planes. +type GCPManagedControlPlaneClassSpec struct { + // MachineTemplate contains information about how machines + // should be shaped when creating or updating a control plane. + // For the GCPManagedControlPlaneTemplate, this field is used + // only to fulfill the CAPI contract. + // +optional + MachineTemplate *GCPManagedControlPlaneTemplateMachineTemplate `json:"machineTemplate,omitempty"` + + // ClusterNetwork define the cluster network. + // +optional + ClusterNetwork *ClusterNetwork `json:"clusterNetwork,omitempty"` + + // Project is the name of the project to deploy the cluster to. + Project string `json:"project"` + + // Location represents the location (region or zone) in which the GKE cluster + // will be created. + Location string `json:"location"` + + // EnableAutopilot indicates whether to enable autopilot for this GKE cluster. + // +optional + EnableAutopilot bool `json:"enableAutopilot"` + + // EnableIdentityService indicates whether to enable Identity Service component for this GKE cluster. + // +optional + EnableIdentityService bool `json:"enableIdentityService"` + + // ReleaseChannel represents the release channel of the GKE cluster. + // +optional + ReleaseChannel *ReleaseChannel `json:"releaseChannel,omitempty"` + + // MasterAuthorizedNetworksConfig represents configuration options for master authorized networks feature of the GKE cluster. + // This feature is disabled if this field is not specified. + // +optional + MasterAuthorizedNetworksConfig *MasterAuthorizedNetworksConfig `json:"master_authorized_networks_config,omitempty"` + + // LoggingService represents configuration of logging service feature of the GKE cluster. + // Possible values: none, logging.googleapis.com/kubernetes (default). + // Value is ignored when enableAutopilot = true. + // +optional + LoggingService *LoggingService `json:"loggingService,omitempty"` + + // MonitoringService represents configuration of monitoring service feature of the GKE cluster. + // Possible values: none, monitoring.googleapis.com/kubernetes (default). + // Value is ignored when enableAutopilot = true. + // +optional + MonitoringService *MonitoringService `json:"monitoringService,omitempty"` +} + +// GCPManagedMachinePoolClassSpec defines the GCPManagedMachinePool properties that may be shared across several GCP managed machinepools. +type GCPManagedMachinePoolClassSpec struct { + // NodePoolName specifies the name of the GKE node pool corresponding to this MachinePool. If you don't specify a name + // then a default name will be created based on the namespace and name of the managed machine pool. + // +optional + NodePoolName string `json:"nodePoolName,omitempty"` + // MachineType is the name of a Google Compute Engine [machine + // type](https://cloud.google.com/compute/docs/machine-types). + // If unspecified, the default machine type is `e2-medium`. + // +optional + MachineType *string `json:"machineType,omitempty"` + // DiskSizeGb is the size of the disk attached to each node, specified in GB. + // The smallest allowed disk size is 10GB. If unspecified, the default disk size is 100GB. + // +optional + DiskSizeGb *int32 `json:"diskSizeGb,omitempty"` + // LocalSsdCount is the number of local SSD disks to be attached to the node. + // +optional + LocalSsdCount *int32 `json:"localSsdCount,omitempty"` + // Scaling specifies scaling for the node pool + // +optional + Scaling *NodePoolAutoScaling `json:"scaling,omitempty"` + // NodeLocations is the list of zones in which the NodePool's + // nodes should be located. + // +optional + NodeLocations []string `json:"nodeLocations,omitempty"` + // ImageType is image type to use for this nodepool. + // +optional + ImageType *string `json:"imageType,omitempty"` + // InstanceType is name of Compute Engine machine type. + // +optional + InstanceType *string `json:"instanceType,omitempty"` + // DiskType is type of the disk attached to each node. + // +optional + DiskType *DiskType `json:"diskType,omitempty"` + // DiskSizeGB is size of the disk attached to each node, + // specified in GB. + // +kubebuilder:validation:Minimum:=10 + // +optional + DiskSizeGB *int64 `json:"diskSizeGB,omitempty"` + // MaxPodsPerNode is constraint enforced on the max num of + // pods per node. + // +kubebuilder:validation:Minimum:=8 + // +kubebuilder:validation:Maximum:=256 + // +optional + MaxPodsPerNode *int64 `json:"maxPodsPerNode,omitempty"` + // NodeNetwork specifies the node network configuration + // options. + // +optional + NodeNetwork NodeNetworkConfig `json:"nodeNetwork,omitempty"` + // NodeSecurity specifies the node security options. + // +optional + NodeSecurity NodeSecurityConfig `json:"nodeSecurity,omitempty"` + // KubernetesLabels specifies the labels to apply to the nodes of the node pool. + // +optional + KubernetesLabels infrav1.Labels `json:"kubernetesLabels,omitempty"` + // KubernetesTaints specifies the taints to apply to the nodes of the node pool. + // +optional + KubernetesTaints Taints `json:"kubernetesTaints,omitempty"` + // AdditionalLabels is an optional set of tags to add to GCP resources managed by the GCP provider, in addition to the + // ones added by default. + // +optional + AdditionalLabels infrav1.Labels `json:"additionalLabels,omitempty"` + // Management specifies the node pool management options. + // +optional + Management *NodePoolManagement `json:"management,omitempty"` + // LinuxNodeConfig specifies the settings for Linux agent nodes. + // +optional + LinuxNodeConfig *LinuxNodeConfig `json:"linuxNodeConfig,omitempty"` +} diff --git a/exp/api/v1beta1/types_template.go b/exp/api/v1beta1/types_template.go new file mode 100644 index 000000000..91626a0f8 --- /dev/null +++ b/exp/api/v1beta1/types_template.go @@ -0,0 +1,78 @@ +/* +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 v1beta1 + +import ( + infrav1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// GCPManagedControlPlaneTemplateResourceSpec specifies an GCP managed control plane template resource. +type GCPManagedControlPlaneTemplateResourceSpec struct { + GCPManagedControlPlaneClassSpec `json:",inline"` +} + +// GCPManagedControlPlaneTemplateMachineTemplate is only used to fulfill the CAPI contract which expects a +// MachineTemplate field for any controlplane ref in a topology. +type GCPManagedControlPlaneTemplateMachineTemplate struct{} + +// GCPManagedMachinePoolTemplateResourceSpec specifies an GCP managed control plane template resource. +type GCPManagedMachinePoolTemplateResourceSpec struct { + GCPManagedMachinePoolClassSpec `json:",inline"` +} + +// GCPManagedClusterTemplateResourceSpec specifies an GCP managed cluster template resource. +type GCPManagedClusterTemplateResourceSpec struct { + // Project is the name of the project to deploy the cluster to. + Project string `json:"project"` + + // The GCP Region the cluster lives in. + Region string `json:"region"` + + // ControlPlaneEndpoint represents the endpoint used to communicate with the control plane. + // +optional + ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"` + + // NetworkSpec encapsulates all things related to the GCP network. + // +optional + Network infrav1.NetworkSpec `json:"network"` + + // AdditionalLabels is an optional set of tags to add to GCP resources managed by the GCP provider, in addition to the + // ones added by default. + // +optional + AdditionalLabels infrav1.Labels `json:"additionalLabels,omitempty"` + + // ResourceManagerTags is an optional set of tags to apply to GCP resources managed + // by the GCP provider. GCP supports a maximum of 50 tags per resource. + // +maxItems=50 + // +optional + ResourceManagerTags infrav1.ResourceManagerTags `json:"resourceManagerTags,omitempty"` + + // CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this cluster. If not + // supplied then the credentials of the controller will be used. + // +optional + CredentialsRef *infrav1.ObjectReference `json:"credentialsRef,omitempty"` + + // LoadBalancerSpec contains configuration for one or more LoadBalancers. + // +optional + LoadBalancer infrav1.LoadBalancerSpec `json:"loadBalancer,omitempty"` + + // ServiceEndpoints contains the custom GCP Service Endpoint urls for each applicable service. + // For instance, the user can specify a new endpoint for the compute service. + // +optional + ServiceEndpoints *infrav1.ServiceEndpoints `json:"serviceEndpoints,omitempty"` +} diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go index 73a813a13..3cd3f63ac 100644 --- a/exp/api/v1beta1/zz_generated.deepcopy.go +++ b/exp/api/v1beta1/zz_generated.deepcopy.go @@ -231,26 +231,25 @@ func (in *GCPManagedClusterStatus) DeepCopy() *GCPManagedClusterStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPManagedControlPlane) DeepCopyInto(out *GCPManagedControlPlane) { +func (in *GCPManagedClusterTemplate) DeepCopyInto(out *GCPManagedClusterTemplate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlane. -func (in *GCPManagedControlPlane) DeepCopy() *GCPManagedControlPlane { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedClusterTemplate. +func (in *GCPManagedClusterTemplate) DeepCopy() *GCPManagedClusterTemplate { if in == nil { return nil } - out := new(GCPManagedControlPlane) + out := new(GCPManagedClusterTemplate) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *GCPManagedControlPlane) DeepCopyObject() runtime.Object { +func (in *GCPManagedClusterTemplate) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -258,31 +257,31 @@ func (in *GCPManagedControlPlane) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPManagedControlPlaneList) DeepCopyInto(out *GCPManagedControlPlaneList) { +func (in *GCPManagedClusterTemplateList) DeepCopyInto(out *GCPManagedClusterTemplateList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]GCPManagedControlPlane, len(*in)) + *out = make([]GCPManagedClusterTemplate, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneList. -func (in *GCPManagedControlPlaneList) DeepCopy() *GCPManagedControlPlaneList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedClusterTemplateList. +func (in *GCPManagedClusterTemplateList) DeepCopy() *GCPManagedClusterTemplateList { if in == nil { return nil } - out := new(GCPManagedControlPlaneList) + out := new(GCPManagedClusterTemplateList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *GCPManagedControlPlaneList) DeepCopyObject() runtime.Object { +func (in *GCPManagedClusterTemplateList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -290,8 +289,112 @@ func (in *GCPManagedControlPlaneList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPManagedControlPlaneSpec) DeepCopyInto(out *GCPManagedControlPlaneSpec) { +func (in *GCPManagedClusterTemplateResource) DeepCopyInto(out *GCPManagedClusterTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedClusterTemplateResource. +func (in *GCPManagedClusterTemplateResource) DeepCopy() *GCPManagedClusterTemplateResource { + if in == nil { + return nil + } + out := new(GCPManagedClusterTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedClusterTemplateResourceSpec) DeepCopyInto(out *GCPManagedClusterTemplateResourceSpec) { *out = *in + out.ControlPlaneEndpoint = in.ControlPlaneEndpoint + in.Network.DeepCopyInto(&out.Network) + if in.AdditionalLabels != nil { + in, out := &in.AdditionalLabels, &out.AdditionalLabels + *out = make(apiv1beta1.Labels, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ResourceManagerTags != nil { + in, out := &in.ResourceManagerTags, &out.ResourceManagerTags + *out = make(apiv1beta1.ResourceManagerTags, len(*in)) + copy(*out, *in) + } + if in.CredentialsRef != nil { + in, out := &in.CredentialsRef, &out.CredentialsRef + *out = new(apiv1beta1.ObjectReference) + **out = **in + } + in.LoadBalancer.DeepCopyInto(&out.LoadBalancer) + if in.ServiceEndpoints != nil { + in, out := &in.ServiceEndpoints, &out.ServiceEndpoints + *out = new(apiv1beta1.ServiceEndpoints) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedClusterTemplateResourceSpec. +func (in *GCPManagedClusterTemplateResourceSpec) DeepCopy() *GCPManagedClusterTemplateResourceSpec { + if in == nil { + return nil + } + out := new(GCPManagedClusterTemplateResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedClusterTemplateSpec) DeepCopyInto(out *GCPManagedClusterTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedClusterTemplateSpec. +func (in *GCPManagedClusterTemplateSpec) DeepCopy() *GCPManagedClusterTemplateSpec { + if in == nil { + return nil + } + out := new(GCPManagedClusterTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedControlPlane) DeepCopyInto(out *GCPManagedControlPlane) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlane. +func (in *GCPManagedControlPlane) DeepCopy() *GCPManagedControlPlane { + if in == nil { + return nil + } + out := new(GCPManagedControlPlane) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GCPManagedControlPlane) 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 *GCPManagedControlPlaneClassSpec) DeepCopyInto(out *GCPManagedControlPlaneClassSpec) { + *out = *in + if in.MachineTemplate != nil { + in, out := &in.MachineTemplate, &out.MachineTemplate + *out = new(GCPManagedControlPlaneTemplateMachineTemplate) + **out = **in + } if in.ClusterNetwork != nil { in, out := &in.ClusterNetwork, &out.ClusterNetwork *out = new(ClusterNetwork) @@ -302,17 +405,6 @@ func (in *GCPManagedControlPlaneSpec) DeepCopyInto(out *GCPManagedControlPlaneSp *out = new(ReleaseChannel) **out = **in } - if in.ControlPlaneVersion != nil { - in, out := &in.ControlPlaneVersion, &out.ControlPlaneVersion - *out = new(string) - **out = **in - } - if in.Version != nil { - in, out := &in.Version, &out.Version - *out = new(string) - **out = **in - } - out.Endpoint = in.Endpoint if in.MasterAuthorizedNetworksConfig != nil { in, out := &in.MasterAuthorizedNetworksConfig, &out.MasterAuthorizedNetworksConfig *out = new(MasterAuthorizedNetworksConfig) @@ -330,6 +422,65 @@ func (in *GCPManagedControlPlaneSpec) DeepCopyInto(out *GCPManagedControlPlaneSp } } +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneClassSpec. +func (in *GCPManagedControlPlaneClassSpec) DeepCopy() *GCPManagedControlPlaneClassSpec { + if in == nil { + return nil + } + out := new(GCPManagedControlPlaneClassSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedControlPlaneList) DeepCopyInto(out *GCPManagedControlPlaneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GCPManagedControlPlane, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneList. +func (in *GCPManagedControlPlaneList) DeepCopy() *GCPManagedControlPlaneList { + if in == nil { + return nil + } + out := new(GCPManagedControlPlaneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GCPManagedControlPlaneList) 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 *GCPManagedControlPlaneSpec) DeepCopyInto(out *GCPManagedControlPlaneSpec) { + *out = *in + in.GCPManagedControlPlaneClassSpec.DeepCopyInto(&out.GCPManagedControlPlaneClassSpec) + if in.ControlPlaneVersion != nil { + in, out := &in.ControlPlaneVersion, &out.ControlPlaneVersion + *out = new(string) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + out.Endpoint = in.Endpoint +} + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneSpec. func (in *GCPManagedControlPlaneSpec) DeepCopy() *GCPManagedControlPlaneSpec { if in == nil { @@ -368,26 +519,25 @@ func (in *GCPManagedControlPlaneStatus) DeepCopy() *GCPManagedControlPlaneStatus } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPManagedMachinePool) DeepCopyInto(out *GCPManagedMachinePool) { +func (in *GCPManagedControlPlaneTemplate) DeepCopyInto(out *GCPManagedControlPlaneTemplate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePool. -func (in *GCPManagedMachinePool) DeepCopy() *GCPManagedMachinePool { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneTemplate. +func (in *GCPManagedControlPlaneTemplate) DeepCopy() *GCPManagedControlPlaneTemplate { if in == nil { return nil } - out := new(GCPManagedMachinePool) + out := new(GCPManagedControlPlaneTemplate) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *GCPManagedMachinePool) DeepCopyObject() runtime.Object { +func (in *GCPManagedControlPlaneTemplate) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -395,31 +545,31 @@ func (in *GCPManagedMachinePool) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPManagedMachinePoolList) DeepCopyInto(out *GCPManagedMachinePoolList) { +func (in *GCPManagedControlPlaneTemplateList) DeepCopyInto(out *GCPManagedControlPlaneTemplateList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]GCPManagedMachinePool, len(*in)) + *out = make([]GCPManagedControlPlaneTemplate, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolList. -func (in *GCPManagedMachinePoolList) DeepCopy() *GCPManagedMachinePoolList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneTemplateList. +func (in *GCPManagedControlPlaneTemplateList) DeepCopy() *GCPManagedControlPlaneTemplateList { if in == nil { return nil } - out := new(GCPManagedMachinePoolList) + out := new(GCPManagedControlPlaneTemplateList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *GCPManagedMachinePoolList) DeepCopyObject() runtime.Object { +func (in *GCPManagedControlPlaneTemplateList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -427,7 +577,97 @@ func (in *GCPManagedMachinePoolList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GCPManagedMachinePoolSpec) DeepCopyInto(out *GCPManagedMachinePoolSpec) { +func (in *GCPManagedControlPlaneTemplateMachineTemplate) DeepCopyInto(out *GCPManagedControlPlaneTemplateMachineTemplate) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneTemplateMachineTemplate. +func (in *GCPManagedControlPlaneTemplateMachineTemplate) DeepCopy() *GCPManagedControlPlaneTemplateMachineTemplate { + if in == nil { + return nil + } + out := new(GCPManagedControlPlaneTemplateMachineTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedControlPlaneTemplateResource) DeepCopyInto(out *GCPManagedControlPlaneTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneTemplateResource. +func (in *GCPManagedControlPlaneTemplateResource) DeepCopy() *GCPManagedControlPlaneTemplateResource { + if in == nil { + return nil + } + out := new(GCPManagedControlPlaneTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedControlPlaneTemplateResourceSpec) DeepCopyInto(out *GCPManagedControlPlaneTemplateResourceSpec) { + *out = *in + in.GCPManagedControlPlaneClassSpec.DeepCopyInto(&out.GCPManagedControlPlaneClassSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneTemplateResourceSpec. +func (in *GCPManagedControlPlaneTemplateResourceSpec) DeepCopy() *GCPManagedControlPlaneTemplateResourceSpec { + if in == nil { + return nil + } + out := new(GCPManagedControlPlaneTemplateResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedControlPlaneTemplateSpec) DeepCopyInto(out *GCPManagedControlPlaneTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedControlPlaneTemplateSpec. +func (in *GCPManagedControlPlaneTemplateSpec) DeepCopy() *GCPManagedControlPlaneTemplateSpec { + if in == nil { + return nil + } + out := new(GCPManagedControlPlaneTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedMachinePool) DeepCopyInto(out *GCPManagedMachinePool) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePool. +func (in *GCPManagedMachinePool) DeepCopy() *GCPManagedMachinePool { + if in == nil { + return nil + } + out := new(GCPManagedMachinePool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GCPManagedMachinePool) 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 *GCPManagedMachinePoolClassSpec) DeepCopyInto(out *GCPManagedMachinePoolClassSpec) { *out = *in if in.MachineType != nil { in, out := &in.MachineType, &out.MachineType @@ -510,6 +750,54 @@ func (in *GCPManagedMachinePoolSpec) DeepCopyInto(out *GCPManagedMachinePoolSpec *out = new(LinuxNodeConfig) (*in).DeepCopyInto(*out) } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolClassSpec. +func (in *GCPManagedMachinePoolClassSpec) DeepCopy() *GCPManagedMachinePoolClassSpec { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolClassSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedMachinePoolList) DeepCopyInto(out *GCPManagedMachinePoolList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GCPManagedMachinePool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolList. +func (in *GCPManagedMachinePoolList) DeepCopy() *GCPManagedMachinePoolList { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GCPManagedMachinePoolList) 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 *GCPManagedMachinePoolSpec) DeepCopyInto(out *GCPManagedMachinePoolSpec) { + *out = *in + in.GCPManagedMachinePoolClassSpec.DeepCopyInto(&out.GCPManagedMachinePoolClassSpec) if in.ProviderIDList != nil { in, out := &in.ProviderIDList, &out.ProviderIDList *out = make([]string, len(*in)) @@ -549,6 +837,112 @@ func (in *GCPManagedMachinePoolStatus) DeepCopy() *GCPManagedMachinePoolStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedMachinePoolTemplate) DeepCopyInto(out *GCPManagedMachinePoolTemplate) { + *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 GCPManagedMachinePoolTemplate. +func (in *GCPManagedMachinePoolTemplate) DeepCopy() *GCPManagedMachinePoolTemplate { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GCPManagedMachinePoolTemplate) 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 *GCPManagedMachinePoolTemplateList) DeepCopyInto(out *GCPManagedMachinePoolTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GCPManagedMachinePoolTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolTemplateList. +func (in *GCPManagedMachinePoolTemplateList) DeepCopy() *GCPManagedMachinePoolTemplateList { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GCPManagedMachinePoolTemplateList) 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 *GCPManagedMachinePoolTemplateResource) DeepCopyInto(out *GCPManagedMachinePoolTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolTemplateResource. +func (in *GCPManagedMachinePoolTemplateResource) DeepCopy() *GCPManagedMachinePoolTemplateResource { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedMachinePoolTemplateResourceSpec) DeepCopyInto(out *GCPManagedMachinePoolTemplateResourceSpec) { + *out = *in + in.GCPManagedMachinePoolClassSpec.DeepCopyInto(&out.GCPManagedMachinePoolClassSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolTemplateResourceSpec. +func (in *GCPManagedMachinePoolTemplateResourceSpec) DeepCopy() *GCPManagedMachinePoolTemplateResourceSpec { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolTemplateResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPManagedMachinePoolTemplateSpec) DeepCopyInto(out *GCPManagedMachinePoolTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPManagedMachinePoolTemplateSpec. +func (in *GCPManagedMachinePoolTemplateSpec) DeepCopy() *GCPManagedMachinePoolTemplateSpec { + if in == nil { + return nil + } + out := new(GCPManagedMachinePoolTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LinuxNodeConfig) DeepCopyInto(out *LinuxNodeConfig) { *out = *in diff --git a/exp/bootstrap/gke/api/v1beta1/gkeconfig_types.go b/exp/bootstrap/gke/api/v1beta1/gkeconfig_types.go new file mode 100644 index 000000000..c53d8dc2c --- /dev/null +++ b/exp/bootstrap/gke/api/v1beta1/gkeconfig_types.go @@ -0,0 +1,81 @@ +/* +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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// GKEConfigSpec defines the desired state of GCP GKE Bootstrap Configuration. +type GKEConfigSpec struct{} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=gkeconfigs,scope=Namespaced,categories=cluster-api,shortName=gkec +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="Bootstrap configuration is ready" +// +kubebuilder:printcolumn:name="DataSecretName",type="string",JSONPath=".status.dataSecretName",description="Name of Secret containing bootstrap data" + +// GKEConfig is the schema for the GCP GKE Bootstrap Configuration. +// this is a placeholder used for compliance with the CAPI contract. +type GKEConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GKEConfigSpec `json:"spec,omitempty"` + Status GKEConfigStatus `json:"status,omitempty"` +} + +// GKEConfigStatus defines the observed state of the GCP GKE Bootstrap Configuration. +type GKEConfigStatus struct { + // Ready indicates the BootstrapData secret is ready to be consumed + Ready bool `json:"ready,omitempty"` + + // DataSecretName is the name of the secret that stores the bootstrap data script. + // +optional + DataSecretName *string `json:"dataSecretName,omitempty"` + + // FailureReason will be set on non-retryable errors + // +optional + FailureReason string `json:"failureReason,omitempty"` + + // FailureMessage will be set on non-retryable errors + // +optional + FailureMessage string `json:"failureMessage,omitempty"` + + // ObservedGeneration is the latest generation observed by the controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Conditions defines current service state of the GKEConfig. + // +optional + Conditions clusterv1.Conditions `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true + +// GKEConfigList contains a list of GCP GKE Bootstrap Configuration. +type GKEConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GKEConfig `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GKEConfig{}, &GKEConfigList{}) +} diff --git a/exp/bootstrap/gke/api/v1beta1/gkeconfigtemplate_types.go b/exp/bootstrap/gke/api/v1beta1/gkeconfigtemplate_types.go new file mode 100644 index 000000000..fc9ee677b --- /dev/null +++ b/exp/bootstrap/gke/api/v1beta1/gkeconfigtemplate_types.go @@ -0,0 +1,54 @@ +/* +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 v1beta1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// GKEConfigTemplateSpec defines the desired state of templated GKEConfig GCP GKE Bootstrap Configuration resources. +type GKEConfigTemplateSpec struct { + Template GKEConfigTemplateResource `json:"template"` +} + +// GKEConfigTemplateResource defines the Template structure. +type GKEConfigTemplateResource struct { + Spec GKEConfigSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:resource:path=gkeconfigtemplates,scope=Namespaced,categories=cluster-api,shortName=gkect + +// GKEConfigTemplate is the GCP GKE Bootstrap Configuration Template API. +type GKEConfigTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GKEConfigTemplateSpec `json:"spec,omitempty"` +} + +// +kubebuilder:object:root=true + +// GKEConfigTemplateList contains a list of GCP GKE Bootstrap Configuration Templates. +type GKEConfigTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GKEConfigTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GKEConfigTemplate{}, &GKEConfigTemplateList{}) +} diff --git a/exp/bootstrap/gke/api/v1beta1/groupversion_info.go b/exp/bootstrap/gke/api/v1beta1/groupversion_info.go new file mode 100644 index 000000000..4b556a22b --- /dev/null +++ b/exp/bootstrap/gke/api/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains API Schema definitions for the infrastructure v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=bootstrap.cluster.x-k8s.io +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "bootstrap.cluster.x-k8s.io", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/exp/bootstrap/gke/api/v1beta1/zz_generated.deepcopy.go b/exp/bootstrap/gke/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000..473a818bd --- /dev/null +++ b/exp/bootstrap/gke/api/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,217 @@ +//go:build !ignore_autogenerated + +/* +Copyright 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + apiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GKEConfig) DeepCopyInto(out *GKEConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfig. +func (in *GKEConfig) DeepCopy() *GKEConfig { + if in == nil { + return nil + } + out := new(GKEConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GKEConfig) 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 *GKEConfigList) DeepCopyInto(out *GKEConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GKEConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfigList. +func (in *GKEConfigList) DeepCopy() *GKEConfigList { + if in == nil { + return nil + } + out := new(GKEConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GKEConfigList) 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 *GKEConfigSpec) DeepCopyInto(out *GKEConfigSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfigSpec. +func (in *GKEConfigSpec) DeepCopy() *GKEConfigSpec { + if in == nil { + return nil + } + out := new(GKEConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GKEConfigStatus) DeepCopyInto(out *GKEConfigStatus) { + *out = *in + if in.DataSecretName != nil { + in, out := &in.DataSecretName, &out.DataSecretName + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(apiv1beta1.Conditions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfigStatus. +func (in *GKEConfigStatus) DeepCopy() *GKEConfigStatus { + if in == nil { + return nil + } + out := new(GKEConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GKEConfigTemplate) DeepCopyInto(out *GKEConfigTemplate) { + *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 GKEConfigTemplate. +func (in *GKEConfigTemplate) DeepCopy() *GKEConfigTemplate { + if in == nil { + return nil + } + out := new(GKEConfigTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GKEConfigTemplate) 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 *GKEConfigTemplateList) DeepCopyInto(out *GKEConfigTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GKEConfigTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfigTemplateList. +func (in *GKEConfigTemplateList) DeepCopy() *GKEConfigTemplateList { + if in == nil { + return nil + } + out := new(GKEConfigTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GKEConfigTemplateList) 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 *GKEConfigTemplateResource) DeepCopyInto(out *GKEConfigTemplateResource) { + *out = *in + out.Spec = in.Spec +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfigTemplateResource. +func (in *GKEConfigTemplateResource) DeepCopy() *GKEConfigTemplateResource { + if in == nil { + return nil + } + out := new(GKEConfigTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GKEConfigTemplateSpec) DeepCopyInto(out *GKEConfigTemplateSpec) { + *out = *in + out.Template = in.Template +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GKEConfigTemplateSpec. +func (in *GKEConfigTemplateSpec) DeepCopy() *GKEConfigTemplateSpec { + if in == nil { + return nil + } + out := new(GKEConfigTemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/exp/bootstrap/gke/controllers/gkeconfig_controller.go b/exp/bootstrap/gke/controllers/gkeconfig_controller.go new file mode 100644 index 000000000..58945bf53 --- /dev/null +++ b/exp/bootstrap/gke/controllers/gkeconfig_controller.go @@ -0,0 +1,182 @@ +/* +Copyright 2020 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 controllers provides a way to reconcile GKEConfig objects. +package controllers + +import ( + "context" + "time" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + + infrav1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/api/v1beta1" + bootstrapv1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/bootstrap/gke/api/v1beta1" + expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" + "sigs.k8s.io/cluster-api/util/predicates" +) + +// GKEConfigReconciler reconciles a GKEConfig object. +type GKEConfigReconciler struct { + client.Client + WatchFilterValue string + ReconcileTimeout time.Duration +} + +// +kubebuilder:rbac:groups=bootstrap.cluster.x-k8s.io,resources=gkeconfigs,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=bootstrap.cluster.x-k8s.io,resources=gkeconfigs/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools;clusters,verbs=get;list;watch + +func (r *GKEConfigReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, option controller.Options) error { + log := ctrl.LoggerFrom(ctx) + + b := ctrl.NewControllerManagedBy(mgr). + For(&bootstrapv1exp.GKEConfig{}). + WithOptions(option). + WithEventFilter(predicates.ResourceHasFilterLabel(mgr.GetScheme(), log, r.WatchFilterValue)). + Watches( + &infrav1exp.GCPManagedMachinePool{}, + handler.EnqueueRequestsFromMapFunc(r.ManagedMachinePoolToGKEConfigMapFunc), + ) + + _, err := b.Build(r) + if err != nil { + return errors.Wrap(err, "failed setting up with a controller manager") + } + + return nil +} + +func (r *GKEConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, rerr error) { + log := ctrl.LoggerFrom(ctx) + + config := &bootstrapv1exp.GKEConfig{} + if err := r.Get(ctx, req.NamespacedName, config); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + log.Error(err, "Failed to get config") + return ctrl.Result{}, err + } + log = log.WithValues("GKEConfig", config.GetName()) + + machinePool, err := getOwnerMachinePool(ctx, r.Client, config.ObjectMeta) + if err != nil { + log.Error(err, "Failed to get owner MachinePool for GKEConfig", "GKEConfig", config.GetName()) + return ctrl.Result{}, err + } + + if machinePool == nil { + log.Info("No owner MachinePool found for GKEConfig", "GKEConfig", config.GetName()) + return ctrl.Result{}, nil + } + + // fetch associated GCPManagedMachinePool + gcpMP := &infrav1exp.GCPManagedMachinePool{} + gcpMPKey := types.NamespacedName{ + Name: machinePool.Spec.Template.Spec.InfrastructureRef.Name, + Namespace: machinePool.Spec.Template.Spec.InfrastructureRef.Namespace, + } + if err := r.Get(ctx, gcpMPKey, gcpMP); err != nil { + if apierrors.IsNotFound(err) { + log.Info("GCPManagedMachinePool not found for MachinePool", "GCPManagedMachinePool", gcpMPKey) + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + // check if GCPManagedMachinePool is ready + if !gcpMP.Status.Ready { + log.Info("Waiting for GCPManagedMachinePool to be ready", "GKEConfig", config.GetName(), "GCPManagedMachinePool", gcpMPKey) + return ctrl.Result{}, nil + } + + // set GKEConfig as ready when GCPManagedMachinePool becomes ready + config.Status.Ready = true + if err := r.Status().Update(ctx, config); err != nil { + log.Info("Failed to update GKEConfig status", "GKEConfig", config.GetName(), "error", err) + return ctrl.Result{}, err + } + + log.Info("Successfully reconciled GKEConfig", "GKEConfig", req.NamespacedName, "MachinePool", machinePool.GetName()) + + return ctrl.Result{}, nil +} + +// ManagedMachinePoolToGKEConfigMapFunc is a handler.ToRequestsFunc to be used to enqueue requests for +// GKEConfig reconciliation. +func (r *GKEConfigReconciler) ManagedMachinePoolToGKEConfigMapFunc(_ context.Context, o client.Object) []ctrl.Request { + c, ok := o.(*infrav1exp.GCPManagedMachinePool) + if !ok { + klog.Errorf("Expected a Cluster but got a %T", o) + } + + machinePool, err := getOwnerMachinePool(context.Background(), r.Client, c.ObjectMeta) + if err != nil { + klog.Errorf("Failed to get owner MachinePool for GCPManagedMachinePool %s/%s: %v", c.Namespace, c.Name, err) + return nil + } + + if machinePool == nil { + klog.Infof("No owner MachinePool found for GCPManagedMachinePool %s/%s", c.Namespace, c.Name) + return nil + } + + return []ctrl.Request{ + { + NamespacedName: client.ObjectKey{ + Name: machinePool.Spec.Template.Spec.InfrastructureRef.Name, + Namespace: machinePool.Spec.Template.Spec.InfrastructureRef.Namespace, + }, + }, + } +} + +func getOwnerMachinePool(ctx context.Context, c client.Client, obj metav1.ObjectMeta) (*expclusterv1.MachinePool, error) { + for _, ref := range obj.OwnerReferences { + if ref.Kind != "MachinePool" { + continue + } + gv, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + return nil, errors.WithStack(err) + } + if gv.Group == expclusterv1.GroupVersion.Group { + return getMachinePoolByName(ctx, c, obj.Namespace, ref.Name) + } + } + + return nil, nil +} + +func getMachinePoolByName(ctx context.Context, c client.Client, namespace, name string) (*expclusterv1.MachinePool, error) { + m := &expclusterv1.MachinePool{} + key := client.ObjectKey{Name: name, Namespace: namespace} + if err := c.Get(ctx, key, m); err != nil { + return nil, err + } + + return m, nil +} diff --git a/exp/controllers/gcpmanagedmachinepool_controller.go b/exp/controllers/gcpmanagedmachinepool_controller.go index 1a90e33ca..3019b12da 100644 --- a/exp/controllers/gcpmanagedmachinepool_controller.go +++ b/exp/controllers/gcpmanagedmachinepool_controller.go @@ -81,7 +81,7 @@ func GetOwnerClusterKey(obj metav1.ObjectMeta) (*client.ObjectKey, error) { } func machinePoolToInfrastructureMapFunc(gvk schema.GroupVersionKind) handler.MapFunc { - return func(_ context.Context, o client.Object) []reconcile.Request { + return func(ctx context.Context, o client.Object) []reconcile.Request { m, ok := o.(*expclusterv1.MachinePool) if !ok { panic(fmt.Sprintf("Expected a MachinePool but got a %T", o)) @@ -91,6 +91,7 @@ func machinePoolToInfrastructureMapFunc(gvk schema.GroupVersionKind) handler.Map // Return early if the GroupKind doesn't match what we expect infraGK := m.Spec.Template.Spec.InfrastructureRef.GroupVersionKind().GroupKind() if gk != infraGK { + log.FromContext(ctx).Info("gk does not match", "gk", gk, "infraGK", infraGK) return nil } @@ -220,6 +221,7 @@ func getOwnerMachinePool(ctx context.Context, c client.Client, obj metav1.Object //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedmachinepools/finalizers,verbs=update //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedcontrolplanes,verbs=get;list;watch //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=gcpmanagedclusters,verbs=get;list;watch +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch;patch //+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinepools;machinepools/status,verbs=get;list;watch //+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch @@ -260,6 +262,8 @@ func (r *GCPManagedMachinePoolReconciler) Reconcile(ctx context.Context, req ctr return ctrl.Result{}, nil } + log.WithValues("ownerCluster", cluster.Name) + // Get the managed cluster gcpManagedClusterKey := client.ObjectKey{ Namespace: gcpManagedMachinePool.Namespace, @@ -321,6 +325,7 @@ func (r *GCPManagedMachinePoolReconciler) reconcile(ctx context.Context, managed log.Info("Reconciling GCPManagedMachinePool") controllerutil.AddFinalizer(managedMachinePoolScope.GCPManagedMachinePool, infrav1exp.ManagedMachinePoolFinalizer) + managedMachinePoolScope.SetInfrastructureMachineKind() if err := managedMachinePoolScope.PatchObject(); err != nil { return ctrl.Result{}, err } diff --git a/main.go b/main.go index e1f1bad51..ad7d4c146 100644 --- a/main.go +++ b/main.go @@ -35,14 +35,18 @@ import ( infrav1beta1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" "sigs.k8s.io/cluster-api-provider-gcp/controllers" infrav1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/api/v1beta1" + gkebootstrapv1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/bootstrap/gke/api/v1beta1" + gkeboostrapcontrollersv1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/bootstrap/gke/controllers" expcontrollers "sigs.k8s.io/cluster-api-provider-gcp/exp/controllers" "sigs.k8s.io/cluster-api-provider-gcp/feature" "sigs.k8s.io/cluster-api-provider-gcp/util/reconciler" "sigs.k8s.io/cluster-api-provider-gcp/version" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" expclusterv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" + capifeature "sigs.k8s.io/cluster-api/feature" "sigs.k8s.io/cluster-api/util/flags" "sigs.k8s.io/cluster-api/util/record" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -63,6 +67,7 @@ func init() { _ = clusterv1.AddToScheme(scheme) _ = expclusterv1.AddToScheme(scheme) _ = infrav1exp.AddToScheme(scheme) + _ = gkebootstrapv1exp.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } @@ -76,6 +81,7 @@ var ( webhookCertDir string gcpClusterConcurrency int gcpMachineConcurrency int + gkeConfigConcurrency int webhookPort int reconcileTimeout time.Duration syncPeriod time.Duration @@ -227,6 +233,16 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager) error { }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: gcpMachineConcurrency}); err != nil { return fmt.Errorf("setting up GCPManagedMachinePool controller: %w", err) } + + if feature.Gates.Enabled(capifeature.MachinePool) { + if err := (&gkeboostrapcontrollersv1exp.GKEConfigReconciler{ + Client: mgr.GetClient(), + ReconcileTimeout: reconcileTimeout, + WatchFilterValue: watchFilterValue, + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: gkeConfigConcurrency}); err != nil { + return fmt.Errorf("setting up GKEConfig controller: %w", err) + } + } } return nil @@ -252,12 +268,21 @@ func setupWebhooks(mgr ctrl.Manager) error { if err := (&infrav1exp.GCPManagedCluster{}).SetupWebhookWithManager(mgr); err != nil { return fmt.Errorf("setting up GCPManagedCluster webhook: %w", err) } + if err := (&infrav1exp.GCPManagedClusterTemplate{}).SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("setting up GCPManagedClusterTemplate webhook: %w", err) + } if err := (&infrav1exp.GCPManagedControlPlane{}).SetupWebhookWithManager(mgr); err != nil { return fmt.Errorf("setting up GCPManagedControlPlane webhook: %w", err) } + if err := (&infrav1exp.GCPManagedControlPlaneTemplate{}).SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("setting up GCPManagedControlPlaneTemplate webhook: %w", err) + } if err := (&infrav1exp.GCPManagedMachinePool{}).SetupWebhookWithManager(mgr); err != nil { return fmt.Errorf("setting up GCPManagedMachinePool webhook: %w", err) } + if err := (&infrav1exp.GCPManagedMachinePoolTemplate{}).SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("setting up GCPManagedMachinePoolTemplate webhook: %w", err) + } } return nil @@ -344,6 +369,12 @@ func initFlags(fs *pflag.FlagSet) { "Number of GCPMachines to process simultaneously", ) + fs.IntVar(&gkeConfigConcurrency, + "gkeconfig-concurrency", + 10, + "Number of GKEConfigs to process simultaneously", + ) + fs.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, diff --git a/templates/cluster-template-gke-autopilot-clusterclass.yaml b/templates/cluster-template-gke-autopilot-clusterclass.yaml new file mode 100644 index 000000000..094cf76ac --- /dev/null +++ b/templates/cluster-template-gke-autopilot-clusterclass.yaml @@ -0,0 +1,89 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: ${CLUSTER_CLASS_NAME} +spec: + controlPlane: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedControlPlaneTemplate + name: ${CLUSTER_NAME}-control-plane + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + name: ${CLUSTER_NAME} + variables: + - name: region + required: true + schema: + openAPIV3Schema: + type: string + default: us-east4 + - name: networkName + required: true + schema: + openAPIV3Schema: + type: string + default: gke-default-network + patches: + - name: managedClusterRegion + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/region + valueFrom: + variable: region + - name: managedControlPlaneRegion + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/location + valueFrom: + variable: region + - name: managedClusterNetwork + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/network/name + valueFrom: + variable: networkName +--- +kind: GCPManagedControlPlaneTemplate +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + template: + spec: + project: "${GCP_PROJECT}" + location: "set-by-patch" + enableAutopilot: true + releaseChannel: "regular" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: GCPManagedClusterTemplate +metadata: + name: ${CLUSTER_NAME} +spec: + template: + spec: + project: "${GCP_PROJECT}" + region: "set-by-patch" + network: + name: "set-by-patch" diff --git a/templates/cluster-template-gke-autopilot-topology.yaml b/templates/cluster-template-gke-autopilot-topology.yaml new file mode 100644 index 000000000..ea3274697 --- /dev/null +++ b/templates/cluster-template-gke-autopilot-topology.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: ${CLUSTER_NAME} +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + topology: + class: ${CLUSTER_CLASS_NAME} + version: ${KUBERNETES_VERSION} + variables: + - name: region + value: ${GCP_REGION} + - name: networkName + value: ${GCP_NETWORK_NAME} diff --git a/templates/cluster-template-gke-clusterclass.yaml b/templates/cluster-template-gke-clusterclass.yaml new file mode 100644 index 000000000..035b09ed6 --- /dev/null +++ b/templates/cluster-template-gke-clusterclass.yaml @@ -0,0 +1,106 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: ${CLUSTER_CLASS_NAME} + namespace: default +spec: + controlPlane: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedControlPlaneTemplate + name: ${CLUSTER_NAME}-control-plane + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + name: ${CLUSTER_NAME} + workers: + machinePools: + - class: default-system + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: GKEConfigTemplate + name: ${CLUSTER_NAME}-pool0 + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedMachinePoolTemplate + name: ${CLUSTER_NAME}-pool0 + - class: default-worker + template: + bootstrap: + ref: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: GKEConfigTemplate + name: ${CLUSTER_NAME}-pool1 + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedMachinePoolTemplate + name: ${CLUSTER_NAME}-pool1 +--- +kind: GCPManagedControlPlaneTemplate +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + template: + spec: + project: "${GCP_PROJECT}" + location: "${GCP_REGION}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: GCPManagedClusterTemplate +metadata: + name: ${CLUSTER_NAME} +spec: + template: + spec: + project: "${GCP_PROJECT}" + region: "${GCP_REGION}" + network: + name: "${GCP_NETWORK_NAME}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: GCPManagedMachinePoolTemplate +metadata: + name: ${CLUSTER_NAME}-pool0 + namespace: default +spec: + template: + spec: + nodePoolName: ${CLUSTER_NAME}-pool0 + nodeLocations: + - "${GCP_REGION}-a" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: GCPManagedMachinePoolTemplate +metadata: + name: ${CLUSTER_NAME}-pool1 + namespace: default +spec: + template: + spec: + nodePoolName: ${CLUSTER_NAME}-pool1 + nodeLocations: + - "${GCP_REGION}-a" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: GKEConfigTemplate +metadata: + name: ${CLUSTER_NAME}-pool0 + namespace: default +spec: + template: + spec: {} +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: GKEConfigTemplate +metadata: + name: ${CLUSTER_NAME}-pool1 + namespace: default +spec: + template: + spec: {} diff --git a/templates/cluster-template-gke-topology.yaml b/templates/cluster-template-gke-topology.yaml new file mode 100644 index 000000000..b97a05ae5 --- /dev/null +++ b/templates/cluster-template-gke-topology.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: ${CLUSTER_NAME} +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + topology: + class: ${CLUSTER_CLASS_NAME} + version: ${KUBERNETES_VERSION} + workers: + machinePools: + - class: default-system + name: mp-0 + replicas: 1 + - class: default-worker + name: mp-1 + replicas: 1 diff --git a/test/e2e/config/gcp-ci.yaml b/test/e2e/config/gcp-ci.yaml index 23057345e..7b7544ef8 100644 --- a/test/e2e/config/gcp-ci.yaml +++ b/test/e2e/config/gcp-ci.yaml @@ -75,6 +75,7 @@ providers: - sourcePath: "${PWD}/test/e2e/data/infrastructure-gcp/cluster-template-ci-gke-autopilot.yaml" - sourcePath: "${PWD}/test/e2e/data/infrastructure-gcp/cluster-template-ci-gke-custom-subnet.yaml" - sourcePath: "${PWD}/test/e2e/data/infrastructure-gcp/cluster-template-ci-with-internal-lb.yaml" + - sourcePath: "${PWD}/test/e2e/data/infrastructure-gcp/withclusterclass/cluster-template-ci-gke-autopilot-topology.yaml" variables: KUBERNETES_VERSION: "v1.32.5" diff --git a/test/e2e/data/infrastructure-gcp/withclusterclass/cluster-template-ci-gke-autopilot-topology.yaml b/test/e2e/data/infrastructure-gcp/withclusterclass/cluster-template-ci-gke-autopilot-topology.yaml new file mode 100644 index 000000000..575cc9678 --- /dev/null +++ b/test/e2e/data/infrastructure-gcp/withclusterclass/cluster-template-ci-gke-autopilot-topology.yaml @@ -0,0 +1,127 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: ClusterClass +metadata: + name: gke-autopilot-cc + namespace: default +spec: + controlPlane: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedControlPlaneTemplate + name: ${CLUSTER_NAME}-control-plane + infrastructure: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + name: ${CLUSTER_NAME} + variables: + - name: region + required: true + schema: + openAPIV3Schema: + type: string + default: us-east4 + - name: networkName + required: true + schema: + openAPIV3Schema: + type: string + default: gke-default-network + patches: + - name: managedClusterRegion + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/region + valueFrom: + variable: region + - name: managedControlPlaneRegion + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedControlPlaneTemplate + matchResources: + controlPlane: true + jsonPatches: + - op: add + path: /spec/template/spec/location + valueFrom: + variable: region + - name: managedClusterNetwork + definitions: + - selector: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 + kind: GCPManagedClusterTemplate + matchResources: + infrastructureCluster: true + jsonPatches: + - op: add + path: /spec/template/spec/network/name + valueFrom: + variable: networkName +--- +kind: GCPManagedControlPlaneTemplate +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +metadata: + name: "${CLUSTER_NAME}-control-plane" +spec: + template: + spec: + project: "${GCP_PROJECT}" + location: "set-by-patch" + enableAutopilot: true + releaseChannel: "regular" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: GCPManagedClusterTemplate +metadata: + name: ${CLUSTER_NAME} +spec: + template: + spec: + project: "${GCP_PROJECT}" + region: "set-by-patch" + network: + name: "set-by-patch" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 +kind: GCPMachineTemplate +metadata: + name: "${CLUSTER_NAME}-md-0" +spec: + template: + spec: + instanceType: "${GCP_MACHINE_TYPE}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: ${CLUSTER_NAME}-md-0 + namespace: default +spec: + template: + spec: {} +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: ${CLUSTER_NAME} +spec: + clusterNetwork: + pods: + cidrBlocks: + - 192.168.0.0/16 + topology: + class: gke-autopilot-cc + version: ${KUBERNETES_VERSION} + variables: + - name: region + value: ${GCP_REGION} + - name: networkName + value: ${GCP_NETWORK_NAME} + diff --git a/test/e2e/e2e_gke_test.go b/test/e2e/e2e_gke_test.go index 78e6ec53e..93ebce706 100644 --- a/test/e2e/e2e_gke_test.go +++ b/test/e2e/e2e_gke_test.go @@ -189,4 +189,29 @@ var _ = Describe("GKE workload cluster creation", func() { }, result) }) }) + + Context("Creating a GKE cluster with autopilot from a cluster class", func() { + It("Should create a cluster class and a cluster from it", func() { + By("Initializes a managed control plane and managed cluster") + + ApplyManagedClusterTemplateAndWait(ctx, ApplyManagedClusterTemplateAndWaitInput{ + ClusterProxy: bootstrapClusterProxy, + ConfigCluster: clusterctl.ConfigClusterInput{ + LogFolder: clusterctlLogFolder, + ClusterctlConfigPath: clusterctlConfigPath, + KubeconfigPath: bootstrapClusterProxy.GetKubeconfigPath(), + InfrastructureProvider: clusterctl.DefaultInfrastructureProvider, + Flavor: "ci-gke-autopilot-topology", + Namespace: namespace.Name, + ClusterName: clusterName, + KubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), + ControlPlaneMachineCount: ptr.To[int64](1), + WorkerMachineCount: ptr.To[int64](0), + }, + WaitForClusterIntervals: e2eConfig.GetIntervals(specName, "wait-cluster"), + WaitForControlPlaneIntervals: e2eConfig.GetIntervals(specName, "wait-control-plane"), + WaitForMachinePools: e2eConfig.GetIntervals(specName, "wait-worker-machine-pools"), + }, result) + }) + }) }) diff --git a/util/webhook/validator.go b/util/webhook/validator.go new file mode 100644 index 000000000..b96ee5ffe --- /dev/null +++ b/util/webhook/validator.go @@ -0,0 +1,63 @@ +/* +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 webhook implements reusable validation functions for webhooks. +package webhook + +import ( + "reflect" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +const ( + unsetMessage = "field is immutable, unable to set an empty value if it was already set" + setMessage = "field is immutable, unable to assign a value if it was already empty" + immutableMessage = "field is immutable" +) + +// ValidateImmutable validates equality across two values, +// and returns a meaningful error to indicate a changed value, a newly set value, or a newly unset value. +func ValidateImmutable(path *field.Path, oldVal, newVal any) *field.Error { + if reflect.TypeOf(oldVal) != reflect.TypeOf(newVal) { + return field.Invalid(path, newVal, "unexpected error") + } + if !reflect.ValueOf(oldVal).IsZero() { + // Prevent modification if it was already set to some value + if reflect.ValueOf(newVal).IsZero() { + // unsetting the field is not allowed + return field.Invalid(path, newVal, unsetMessage) + } + if !reflect.DeepEqual(oldVal, newVal) { + // changing the field is not allowed + return field.Invalid(path, newVal, immutableMessage) + } + } else if !reflect.ValueOf(newVal).IsZero() { + return field.Invalid(path, newVal, setMessage) + } + + return nil +} + +// ValidateNonNegative checks if a numeric value is non-negative, +// and returns a meaningul error to indicate that it is. +func ValidateNonNegative[T int | int32 | int64](path *field.Path, value *T) *field.Error { + if value != nil && *value < 0 { + return field.Invalid(path, value, "must be non-negative") + } + + return nil +}