diff --git a/Makefile b/Makefile
index 286c7be33e..6fba683261 100644
--- a/Makefile
+++ b/Makefile
@@ -333,6 +333,7 @@ generate-codegen: generate-controller-gen $(OPENAPI_GEN) $(APPLYCONFIGURATION_GE
k8s.io/api/core/v1 \
k8s.io/apimachinery/pkg/apis/meta/v1 \
k8s.io/apimachinery/pkg/runtime \
+ k8s.io/apimachinery/pkg/api/resource \
k8s.io/apimachinery/pkg/version
@echo "** Generating openapi.json **"
go run ./cmd/models-schema | jq > ./openapi.json
diff --git a/api/v1beta1/openstackmachinetemplate_types.go b/api/v1beta1/openstackmachinetemplate_types.go
index a02f021025..334b9d40bc 100644
--- a/api/v1beta1/openstackmachinetemplate_types.go
+++ b/api/v1beta1/openstackmachinetemplate_types.go
@@ -17,6 +17,7 @@ limitations under the License.
package v1beta1
import (
+ corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -25,17 +26,39 @@ type OpenStackMachineTemplateSpec struct {
Template OpenStackMachineTemplateResource `json:"template"`
}
+// OpenStackMachineTemplateStatus defines the observed state of OpenStackMachineTemplate.
+type OpenStackMachineTemplateStatus struct {
+ // Capacity defines the resource capacity for this machine.
+ // This value is used for autoscaling from zero operations as defined in:
+ // https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md
+ // +optional
+ Capacity corev1.ResourceList `json:"capacity,omitempty"`
+ // +optional
+ NodeInfo NodeInfo `json:"nodeInfo,omitempty,omitzero"`
+}
+
+// NodeInfo contains information about the node's architecture and operating system.
+// +kubebuilder:validation:MinProperties=1
+type NodeInfo struct {
+ // operatingSystem is a string representing the operating system of the node.
+ // This may be a string like 'linux' or 'windows'.
+ // +optional
+ OperatingSystem string `json:"operatingSystem,omitempty"`
+}
+
// +genclient
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:resource:path=openstackmachinetemplates,scope=Namespaced,categories=cluster-api,shortName=osmt
+// +kubebuilder:subresource:status
// OpenStackMachineTemplate is the Schema for the openstackmachinetemplates API.
type OpenStackMachineTemplate struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
- Spec OpenStackMachineTemplateSpec `json:"spec,omitempty"`
+ Spec OpenStackMachineTemplateSpec `json:"spec,omitempty"`
+ Status OpenStackMachineTemplateStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
@@ -50,3 +73,11 @@ type OpenStackMachineTemplateList struct {
func init() {
objectTypes = append(objectTypes, &OpenStackMachineTemplate{}, &OpenStackMachineTemplateList{})
}
+
+// GetIdentifyRef returns the object's namespace and IdentityRef if it has an IdentityRef, or nulls if it does not.
+func (r *OpenStackMachineTemplate) GetIdentityRef() (*string, *OpenStackIdentityReference) {
+ if r.Spec.Template.Spec.IdentityRef != nil {
+ return &r.Namespace, r.Spec.Template.Spec.IdentityRef
+ }
+ return nil, nil
+}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 0ff6155cab..269ec1c968 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -596,6 +596,21 @@ func (in *NetworkStatusWithSubnets) DeepCopy() *NetworkStatusWithSubnets {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeInfo) DeepCopyInto(out *NodeInfo) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeInfo.
+func (in *NodeInfo) DeepCopy() *NodeInfo {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeInfo)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OpenStackCluster) DeepCopyInto(out *OpenStackCluster) {
*out = *in
@@ -1182,6 +1197,7 @@ func (in *OpenStackMachineTemplate) DeepCopyInto(out *OpenStackMachineTemplate)
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 OpenStackMachineTemplate.
@@ -1266,6 +1282,29 @@ func (in *OpenStackMachineTemplateSpec) DeepCopy() *OpenStackMachineTemplateSpec
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OpenStackMachineTemplateStatus) DeepCopyInto(out *OpenStackMachineTemplateStatus) {
+ *out = *in
+ if in.Capacity != nil {
+ in, out := &in.Capacity, &out.Capacity
+ *out = make(v1.ResourceList, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val.DeepCopy()
+ }
+ }
+ out.NodeInfo = in.NodeInfo
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackMachineTemplateStatus.
+func (in *OpenStackMachineTemplateStatus) DeepCopy() *OpenStackMachineTemplateStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(OpenStackMachineTemplateStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PortOpts) DeepCopyInto(out *PortOpts) {
*out = *in
diff --git a/api_violations.report b/api_violations.report
index baf94dfeec..58ae60e9d2 100644
--- a/api_violations.report
+++ b/api_violations.report
@@ -73,6 +73,12 @@ API rule violation: names_match,k8s.io/api/core/v1,RBDVolumeSource,RBDPool
API rule violation: names_match,k8s.io/api/core/v1,RBDVolumeSource,RadosUser
API rule violation: names_match,k8s.io/api/core/v1,VolumeSource,CephFS
API rule violation: names_match,k8s.io/api/core/v1,VolumeSource,StorageOS
+API rule violation: names_match,k8s.io/apimachinery/pkg/api/resource,Quantity,Format
+API rule violation: names_match,k8s.io/apimachinery/pkg/api/resource,Quantity,d
+API rule violation: names_match,k8s.io/apimachinery/pkg/api/resource,Quantity,i
+API rule violation: names_match,k8s.io/apimachinery/pkg/api/resource,Quantity,s
+API rule violation: names_match,k8s.io/apimachinery/pkg/api/resource,int64Amount,scale
+API rule violation: names_match,k8s.io/apimachinery/pkg/api/resource,int64Amount,value
API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,APIResourceList,APIResources
API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,Duration,Duration
API rule violation: names_match,k8s.io/apimachinery/pkg/apis/meta/v1,InternalEvent,Object
diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go
index 84609b7d86..5b8de98397 100644
--- a/cmd/models-schema/zz_generated.openapi.go
+++ b/cmd/models-schema/zz_generated.openapi.go
@@ -23,6 +23,7 @@ package main
import (
v1 "k8s.io/api/core/v1"
+ resource "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
@@ -262,6 +263,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"k8s.io/api/core/v1.VsphereVirtualDiskVolumeSource": schema_k8sio_api_core_v1_VsphereVirtualDiskVolumeSource(ref),
"k8s.io/api/core/v1.WeightedPodAffinityTerm": schema_k8sio_api_core_v1_WeightedPodAffinityTerm(ref),
"k8s.io/api/core/v1.WindowsSecurityContextOptions": schema_k8sio_api_core_v1_WindowsSecurityContextOptions(ref),
+ "k8s.io/apimachinery/pkg/api/resource.Quantity": schema_apimachinery_pkg_api_resource_Quantity(ref),
+ "k8s.io/apimachinery/pkg/api/resource.int64Amount": schema_apimachinery_pkg_api_resource_int64Amount(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroup": schema_pkg_apis_meta_v1_APIGroup(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIGroupList": schema_pkg_apis_meta_v1_APIGroupList(ref),
"k8s.io/apimachinery/pkg/apis/meta/v1.APIResource": schema_pkg_apis_meta_v1_APIResource(ref),
@@ -352,6 +355,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NetworkParam": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_NetworkParam(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NetworkStatus": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_NetworkStatus(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NetworkStatusWithSubnets": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_NetworkStatusWithSubnets(ref),
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.NodeInfo": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_NodeInfo(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackCluster": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackCluster(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackClusterList": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackClusterList(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackClusterSpec": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackClusterSpec(ref),
@@ -369,6 +373,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackMachineTemplateList": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachineTemplateList(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackMachineTemplateResource": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachineTemplateResource(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackMachineTemplateSpec": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachineTemplateSpec(ref),
+ "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.OpenStackMachineTemplateStatus": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_OpenStackMachineTemplateStatus(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.PortOpts": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortOpts(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.PortStatus": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_PortStatus(ref),
"sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1.ResolvedFixedIP": schema_sigsk8sio_cluster_api_provider_openstack_api_v1beta1_ResolvedFixedIP(ref),
@@ -14133,6 +14138,54 @@ func schema_k8sio_api_core_v1_WindowsSecurityContextOptions(ref common.Reference
}
}
+func schema_apimachinery_pkg_api_resource_Quantity(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.EmbedOpenAPIDefinitionIntoV2Extension(common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "Quantity is a fixed-point representation of a number. It provides convenient marshaling/unmarshaling in JSON and YAML, in addition to String() and AsInt64() accessors.\n\nThe serialization format is:\n\n``` NeutronTag represents a tag on a Neutron resource.
It may not be empty and may not contain commas.
+
+
+status
+
+
+OpenStackMachineTemplateStatus
+
+
+
+
+APIServerLoadBalancer
@@ -2321,6 +2333,38 @@ NetworkStatus
+(Appears on: +OpenStackMachineTemplateStatus) +
++
NodeInfo contains information about the node’s architecture and operating system.
+ +| Field | +Description | +
|---|---|
+operatingSystem+ +string + + |
+
+(Optional)
+ operatingSystem is a string representing the operating system of the node. +This may be a string like ‘linux’ or ‘windows’. + |
+
@@ -4070,6 +4114,52 @@ OpenStackMachineTemplateResource +
+(Appears on: +OpenStackMachineTemplate) +
++
OpenStackMachineTemplateStatus defines the observed state of OpenStackMachineTemplate.
+ +| Field | +Description | +
|---|---|
+capacity+ +Kubernetes core/v1.ResourceList + + |
+
+(Optional)
+ Capacity defines the resource capacity for this machine. +This value is used for autoscaling from zero operations as defined in: +https://github.com/kubernetes-sigs/cluster-api/blob/main/docs/proposals/20210310-opt-in-autoscaling-from-zero.md + |
+
+nodeInfo,omitempty,omitzero+ + +NodeInfo + + + |
++(Optional) + | +
diff --git a/docs/book/src/experimental-features/autoscale-from-zero.md b/docs/book/src/experimental-features/autoscale-from-zero.md new file mode 100644 index 0000000000..6ef8a19134 --- /dev/null +++ b/docs/book/src/experimental-features/autoscale-from-zero.md @@ -0,0 +1,39 @@ +# AutoScale From Zero + +> **Note:** AutoScaleFromZero is available in >= 0.14. + +> **Note**: AutoScaleFromZero can be used only in clusters using the [ClusterClass](https://cluster-api.sigs.k8s.io/tasks/experimental-features/cluster-class/) feature. + +The `AutoScaleFromZero` feature flag enables the usage of [cluster-autoscaler](https://github.com/kubernetes/autoscaler/tree/bc3f44c85df17bccc940adb7c885b192cf6135d7/cluster-autoscaler/cloudprovider/clusterapi#cluster-autoscaler-on-cluster-api) to scale from/to zero without the need of annotations. More information on how to use the cluster-autoscaler can be found [here](https://github.com/kubernetes/autoscaler/tree/bc3f44c85df17bccc940adb7c885b192cf6135d7/cluster-autoscaler/cloudprovider/clusterapi#scale-from-zero-support). + +## Enabling AutoScaleFromZero + +You can enable `AutoScaleFromZero` using the following. + +- Environment variable: `EXP_CAPO_AUTOSCALE_FROM_ZERO=true` +- clusterctl.yaml variable: `EXP_CAPO_AUTOSCALE_FROM_ZERO: true` +- --feature-gates argument: `AutoScaleFromZero=true` + +## Automatically Populated Status Fields + +> **Note**: Unsupported fields may be provided via annotations or incorporated into the controller by extending its functionality. + +The controller automatically fills two sections of `OpenStackMachineTemplate.Status`: +- **capacity** (resource quantities) +- **nodeInfo** (OS metadata) + +The following mappings describe exactly where each value originates. + +### Capacity (`Status.Capacity`) +- **CPU**: From the `VCPUs` property of the resolved OpenStack flavor + +- **Memory**: From the `RAM` property of the resolved OpenStack flavor + +- **Ephemeral Storage**: From the `Ephemeral` property of the resolved OpenStack flavor + +- **Root Storage**: Determined based on the boot method: + - If **booting from volume** taken from `OpenStackMachineTemplate.Spec.Template.Spec.RootVolume.SizeGiB` + - If **booting from image** taken from the `Disk` property of the resolved OpenStack flavor + +### Node Information (`Status.NodeInfo`) +- **Operating System**: From the `os_type` property of the resolved OpenStack image. diff --git a/docs/book/src/experimental-features/experimental-features.md b/docs/book/src/experimental-features/experimental-features.md index 384e140a70..b9cc4fc031 100644 --- a/docs/book/src/experimental-features/experimental-features.md +++ b/docs/book/src/experimental-features/experimental-features.md @@ -4,6 +4,7 @@ CAPO now ships with experimental features the users can enable. Currently CAPO has the following experimental features: * `PriorityQueue` (env var: `EXP_CAPO_PRIORITY_QUEUE`): [PriorityQueue](./priority-queue.md) +* `AutoScaleFromZero` (env var: `EXP_CAPO_AUTOSCALE_FROM_ZERO`): [AutoScaleFromZero](./autoscale-from-zero.md) ## Enabling Experimental Features for Management Clusters Started with clusterctl diff --git a/feature/feature.go b/feature/feature.go index 8da931c1dc..a0286cebd5 100644 --- a/feature/feature.go +++ b/feature/feature.go @@ -34,6 +34,13 @@ const ( // // alpha: v0.14 PriorityQueue featuregate.Feature = "PriorityQueue" + + // AutoScaleFromZero is a feature gate that enables the OpenStackMachineTemplate controller that adds + // information in OpenStackMachineTemplate.status required by the cluster-autoscaler to scale from zero + // without the addition of labels + // + // alpha: v0.14 + AutoScaleFromZero featuregate.Feature = "AutoScaleFromZero" ) func init() { @@ -44,5 +51,6 @@ func init() { // To add a new feature, define a key for it above and add it here. var defaultCAPOFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ // Every feature should be initiated here: - PriorityQueue: {Default: false, PreRelease: featuregate.Alpha}, + PriorityQueue: {Default: false, PreRelease: featuregate.Alpha}, + AutoScaleFromZero: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/main.go b/main.go index fdefb41c41..cf37f8a1f7 100644 --- a/main.go +++ b/main.go @@ -77,28 +77,29 @@ var ( setupLog = ctrl.Log.WithName("setup") // flags. - managerOptions = flags.ManagerOptions{} - enableLeaderElection bool - leaderElectionLeaseDuration time.Duration - leaderElectionRenewDeadline time.Duration - leaderElectionRetryPeriod time.Duration - watchNamespace string - watchFilterValue string - profilerAddress string - openStackClusterConcurrency int - openStackMachineConcurrency int - syncPeriod time.Duration - restConfigQPS float32 - restConfigBurst int - webhookPort int - webhookCertDir string - healthAddr string - lbProvider string - caCertsPath string - showVersion bool - scopeCacheMaxSize int - skipCRDMigrationPhases []string - logOptions = logs.NewOptions() + managerOptions = flags.ManagerOptions{} + enableLeaderElection bool + leaderElectionLeaseDuration time.Duration + leaderElectionRenewDeadline time.Duration + leaderElectionRetryPeriod time.Duration + watchNamespace string + watchFilterValue string + profilerAddress string + openStackClusterConcurrency int + openStackMachineConcurrency int + openStackMachineTemplateConcurrency int + syncPeriod time.Duration + restConfigQPS float32 + restConfigBurst int + webhookPort int + webhookCertDir string + healthAddr string + lbProvider string + caCertsPath string + showVersion bool + scopeCacheMaxSize int + skipCRDMigrationPhases []string + logOptions = logs.NewOptions() ) func init() { @@ -148,6 +149,9 @@ func InitFlags(fs *pflag.FlagSet) { fs.IntVar(&openStackMachineConcurrency, "openstackmachine-concurrency", 10, "Number of OpenStackMachines to process simultaneously") + fs.IntVar(&openStackMachineTemplateConcurrency, "openstackmachinetemplate-concurrency", 10, + "Number of OpenStackMachineTemplates to process simultaneously") + fs.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "The minimum interval at which watched resources are reconciled (e.g. 15m)") @@ -395,6 +399,19 @@ func setupReconcilers(ctx context.Context, mgr ctrl.Manager, caCerts []byte) { setupLog.Error(err, "unable to create controller", "controller", "OpenStackServer") os.Exit(1) } + + if feature.Gates.Enabled(feature.AutoScaleFromZero) { + if err := (&controllers.OpenStackMachineTemplateReconciler{ + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("openstackmachinetemplate-controller"), + WatchFilterValue: watchFilterValue, + ScopeFactory: scopeFactory, + CaCertificates: caCerts, + }).SetupWithManager(ctx, mgr, concurrency(openStackMachineTemplateConcurrency)); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OpenStackMachineTemplate") + os.Exit(1) + } + } } func setupWebhooks(mgr ctrl.Manager) { diff --git a/pkg/clients/compute.go b/pkg/clients/compute.go index 0338417b93..519fdb173d 100644 --- a/pkg/clients/compute.go +++ b/pkg/clients/compute.go @@ -59,6 +59,8 @@ type ComputeClient interface { ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) ListFlavors() ([]flavors.Flavor, error) + GetFlavor(flavorID string) (*flavors.Flavor, error) + CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) DeleteServer(serverID string) error GetServer(serverID string) (*servers.Server, error) @@ -126,6 +128,15 @@ func (c computeClient) ListFlavors() ([]flavors.Flavor, error) { return flavors.ExtractFlavors(allPages) } +func (c computeClient) GetFlavor(flavorID string) (*flavors.Flavor, error) { + mc := metrics.NewMetricPrometheusContext("flavor", "get") + flavor, err := flavors.Get(context.TODO(), c.client, flavorID).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return flavor, nil +} + func (c computeClient) CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { mc := metrics.NewMetricPrometheusContext("server", "create") server, err := servers.Create(context.TODO(), c.client, createOpts, schedulerHints).Extract() @@ -222,6 +233,10 @@ func (e computeErrorClient) ListFlavors() ([]flavors.Flavor, error) { return nil, e.error } +func (e computeErrorClient) GetFlavor(_ string) (*flavors.Flavor, error) { + return nil, e.error +} + func (e computeErrorClient) CreateServer(_ servers.CreateOptsBuilder, _ servers.SchedulerHintOptsBuilder) (*servers.Server, error) { return nil, e.error } diff --git a/pkg/clients/mock/compute.go b/pkg/clients/mock/compute.go index c83d7a701e..6ddd94b6ea 100644 --- a/pkg/clients/mock/compute.go +++ b/pkg/clients/mock/compute.go @@ -118,6 +118,21 @@ func (mr *MockComputeClientMockRecorder) GetConsoleOutput(serverID any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsoleOutput", reflect.TypeOf((*MockComputeClient)(nil).GetConsoleOutput), serverID) } +// GetFlavor mocks base method. +func (m *MockComputeClient) GetFlavor(flavorID string) (*flavors.Flavor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFlavor", flavorID) + ret0, _ := ret[0].(*flavors.Flavor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFlavor indicates an expected call of GetFlavor. +func (mr *MockComputeClientMockRecorder) GetFlavor(flavorID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlavor", reflect.TypeOf((*MockComputeClient)(nil).GetFlavor), flavorID) +} + // GetServer mocks base method. func (m *MockComputeClient) GetServer(serverID string) (*servers.Server, error) { m.ctrl.T.Helper() diff --git a/pkg/cloud/services/compute/instance.go b/pkg/cloud/services/compute/instance.go index e7389a4d9b..9b89d60c50 100644 --- a/pkg/cloud/services/compute/instance.go +++ b/pkg/cloud/services/compute/instance.go @@ -25,8 +25,10 @@ import ( "time" "github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/keypairs" "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -449,6 +451,14 @@ func (s *Service) GetFlavorID(flavorID, flavorName *string) (string, error) { return "", fmt.Errorf("no flavors were found: name=%v", *flavorName) } +func (s *Service) GetFlavor(flavorID string) (*flavors.Flavor, error) { + return s.getComputeClient().GetFlavor(flavorID) +} + +func (s *Service) GetImageDetails(imageID string) (*images.Image, error) { + return s.getImageClient().GetImage(imageID) +} + // GetManagementPort returns the port which is used for management and external // traffic. Cluster floating IPs must be associated with this port. func (s *Service) GetManagementPort(openStackCluster *infrav1.OpenStackCluster, instanceStatus *InstanceStatus) (*ports.Port, error) { diff --git a/pkg/generated/applyconfiguration/api/v1beta1/nodeinfo.go b/pkg/generated/applyconfiguration/api/v1beta1/nodeinfo.go new file mode 100644 index 0000000000..b26f3730bb --- /dev/null +++ b/pkg/generated/applyconfiguration/api/v1beta1/nodeinfo.go @@ -0,0 +1,39 @@ +/* +Copyright 2024 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 applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// NodeInfoApplyConfiguration represents a declarative configuration of the NodeInfo type for use +// with apply. +type NodeInfoApplyConfiguration struct { + OperatingSystem *string `json:"operatingSystem,omitempty"` +} + +// NodeInfoApplyConfiguration constructs a declarative configuration of the NodeInfo type for use with +// apply. +func NodeInfo() *NodeInfoApplyConfiguration { + return &NodeInfoApplyConfiguration{} +} + +// WithOperatingSystem sets the OperatingSystem field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the OperatingSystem field is set to the value of the last call. +func (b *NodeInfoApplyConfiguration) WithOperatingSystem(value string) *NodeInfoApplyConfiguration { + b.OperatingSystem = &value + return b +} diff --git a/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplate.go b/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplate.go index 2f705cd4f5..c6c021bf4e 100644 --- a/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplate.go +++ b/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplate.go @@ -32,7 +32,8 @@ import ( type OpenStackMachineTemplateApplyConfiguration struct { v1.TypeMetaApplyConfiguration `json:",inline"` *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` - Spec *OpenStackMachineTemplateSpecApplyConfiguration `json:"spec,omitempty"` + Spec *OpenStackMachineTemplateSpecApplyConfiguration `json:"spec,omitempty"` + Status *OpenStackMachineTemplateStatusApplyConfiguration `json:"status,omitempty"` } // OpenStackMachineTemplate constructs a declarative configuration of the OpenStackMachineTemplate type for use with @@ -248,6 +249,14 @@ func (b *OpenStackMachineTemplateApplyConfiguration) WithSpec(value *OpenStackMa return b } +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *OpenStackMachineTemplateApplyConfiguration) WithStatus(value *OpenStackMachineTemplateStatusApplyConfiguration) *OpenStackMachineTemplateApplyConfiguration { + b.Status = value + return b +} + // GetName retrieves the value of the Name field in the declarative configuration. func (b *OpenStackMachineTemplateApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() diff --git a/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplatestatus.go b/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplatestatus.go new file mode 100644 index 0000000000..eab3366465 --- /dev/null +++ b/pkg/generated/applyconfiguration/api/v1beta1/openstackmachinetemplatestatus.go @@ -0,0 +1,52 @@ +/* +Copyright 2024 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 applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1 "k8s.io/api/core/v1" +) + +// OpenStackMachineTemplateStatusApplyConfiguration represents a declarative configuration of the OpenStackMachineTemplateStatus type for use +// with apply. +type OpenStackMachineTemplateStatusApplyConfiguration struct { + Capacity *v1.ResourceList `json:"capacity,omitempty"` + NodeInfo *NodeInfoApplyConfiguration `json:"nodeInfo,omitempty"` +} + +// OpenStackMachineTemplateStatusApplyConfiguration constructs a declarative configuration of the OpenStackMachineTemplateStatus type for use with +// apply. +func OpenStackMachineTemplateStatus() *OpenStackMachineTemplateStatusApplyConfiguration { + return &OpenStackMachineTemplateStatusApplyConfiguration{} +} + +// WithCapacity sets the Capacity field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Capacity field is set to the value of the last call. +func (b *OpenStackMachineTemplateStatusApplyConfiguration) WithCapacity(value v1.ResourceList) *OpenStackMachineTemplateStatusApplyConfiguration { + b.Capacity = &value + return b +} + +// WithNodeInfo sets the NodeInfo field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the NodeInfo field is set to the value of the last call. +func (b *OpenStackMachineTemplateStatusApplyConfiguration) WithNodeInfo(value *NodeInfoApplyConfiguration) *OpenStackMachineTemplateStatusApplyConfiguration { + b.NodeInfo = value + return b +} diff --git a/pkg/generated/applyconfiguration/internal/internal.go b/pkg/generated/applyconfiguration/internal/internal.go index deadb79120..2b68e5773b 100644 --- a/pkg/generated/applyconfiguration/internal/internal.go +++ b/pkg/generated/applyconfiguration/internal/internal.go @@ -73,6 +73,8 @@ var schemaYAML = typed.YAMLObject(`types: scalar: string default: "" elementRelationship: atomic +- name: io.k8s.apimachinery.pkg.api.resource.Quantity + scalar: untyped - name: io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1 map: elementType: @@ -806,6 +808,12 @@ var schemaYAML = typed.YAMLObject(`types: elementType: scalar: string elementRelationship: atomic +- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.NodeInfo + map: + fields: + - name: operatingSystem + type: + scalar: string - name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.OpenStackCluster map: fields: @@ -1162,6 +1170,10 @@ var schemaYAML = typed.YAMLObject(`types: type: namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.OpenStackMachineTemplateSpec default: {} + - name: status + type: + namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.OpenStackMachineTemplateStatus + default: {} - name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.OpenStackMachineTemplateResource map: fields: @@ -1176,6 +1188,18 @@ var schemaYAML = typed.YAMLObject(`types: type: namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.OpenStackMachineTemplateResource default: {} +- name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.OpenStackMachineTemplateStatus + map: + fields: + - name: capacity + type: + map: + elementType: + namedType: io.k8s.apimachinery.pkg.api.resource.Quantity + - name: nodeInfo + type: + namedType: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.NodeInfo + default: {} - name: io.k8s.sigs.cluster-api-provider-openstack.api.v1beta1.PortOpts map: fields: diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go index b0ba50dcaa..f65edec9b7 100644 --- a/pkg/generated/applyconfiguration/utils.go +++ b/pkg/generated/applyconfiguration/utils.go @@ -98,6 +98,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1beta1.NetworkStatusApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("NetworkStatusWithSubnets"): return &apiv1beta1.NetworkStatusWithSubnetsApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("NodeInfo"): + return &apiv1beta1.NodeInfoApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("OpenStackCluster"): return &apiv1beta1.OpenStackClusterApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("OpenStackClusterSpec"): @@ -124,6 +126,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1beta1.OpenStackMachineTemplateResourceApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("OpenStackMachineTemplateSpec"): return &apiv1beta1.OpenStackMachineTemplateSpecApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("OpenStackMachineTemplateStatus"): + return &apiv1beta1.OpenStackMachineTemplateStatusApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PortOpts"): return &apiv1beta1.PortOptsApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PortStatus"): diff --git a/pkg/generated/clientset/clientset/typed/api/v1beta1/openstackmachinetemplate.go b/pkg/generated/clientset/clientset/typed/api/v1beta1/openstackmachinetemplate.go index ca49cb02ab..7d7f24bb6b 100644 --- a/pkg/generated/clientset/clientset/typed/api/v1beta1/openstackmachinetemplate.go +++ b/pkg/generated/clientset/clientset/typed/api/v1beta1/openstackmachinetemplate.go @@ -40,6 +40,8 @@ type OpenStackMachineTemplatesGetter interface { type OpenStackMachineTemplateInterface interface { Create(ctx context.Context, openStackMachineTemplate *apiv1beta1.OpenStackMachineTemplate, opts v1.CreateOptions) (*apiv1beta1.OpenStackMachineTemplate, error) Update(ctx context.Context, openStackMachineTemplate *apiv1beta1.OpenStackMachineTemplate, opts v1.UpdateOptions) (*apiv1beta1.OpenStackMachineTemplate, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, openStackMachineTemplate *apiv1beta1.OpenStackMachineTemplate, opts v1.UpdateOptions) (*apiv1beta1.OpenStackMachineTemplate, error) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1beta1.OpenStackMachineTemplate, error) @@ -47,6 +49,8 @@ type OpenStackMachineTemplateInterface interface { Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1beta1.OpenStackMachineTemplate, err error) Apply(ctx context.Context, openStackMachineTemplate *applyconfigurationapiv1beta1.OpenStackMachineTemplateApplyConfiguration, opts v1.ApplyOptions) (result *apiv1beta1.OpenStackMachineTemplate, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, openStackMachineTemplate *applyconfigurationapiv1beta1.OpenStackMachineTemplateApplyConfiguration, opts v1.ApplyOptions) (result *apiv1beta1.OpenStackMachineTemplate, err error) OpenStackMachineTemplateExpansion } diff --git a/pkg/utils/controllers/controllers.go b/pkg/utils/controllers/controllers.go index 8c5f8f9fa2..11d0684352 100644 --- a/pkg/utils/controllers/controllers.go +++ b/pkg/utils/controllers/controllers.go @@ -17,9 +17,13 @@ limitations under the License. package controllers import ( + "context" "fmt" "net" + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + "sigs.k8s.io/controller-runtime/pkg/client" + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" ) @@ -46,3 +50,15 @@ func ValidateSubnets(subnets []infrav1.Subnet) error { } return nil } + +func GetInfraCluster(ctx context.Context, c client.Client, cluster *clusterv1.Cluster, namespace string) (*infrav1.OpenStackCluster, error) { + openStackCluster := &infrav1.OpenStackCluster{} + openStackClusterName := client.ObjectKey{ + Namespace: namespace, + Name: cluster.Spec.InfrastructureRef.Name, + } + if err := c.Get(ctx, openStackClusterName, openStackCluster); err != nil { + return nil, err + } + return openStackCluster, nil +} diff --git a/test/e2e/data/e2e_conf.yaml b/test/e2e/data/e2e_conf.yaml index 6f7e638112..e9ad73b555 100644 --- a/test/e2e/data/e2e_conf.yaml +++ b/test/e2e/data/e2e_conf.yaml @@ -213,6 +213,7 @@ variables: KUBERNETES_VERSION: "v1.34.2" KUBERNETES_VERSION_UPGRADE_FROM: "v1.33.1" KUBERNETES_VERSION_UPGRADE_TO: "v1.34.2" + AUTOSCALER_VERSION: "v1.34.2" # NOTE: To see default images run kubeadm config images list (optionally with --kubernetes-version=vX.Y.Z) ETCD_VERSION_UPGRADE_TO: "3.5.21-0" COREDNS_VERSION_UPGRADE_TO: "v1.12.0" @@ -222,6 +223,7 @@ variables: CCM: "../../data/ccm/cloud-controller-manager.yaml" EXP_CLUSTER_RESOURCE_SET: "true" EXP_CAPO_PRIORITY_QUEUE: "false" + EXP_CAPO_AUTOSCALE_FROM_ZERO: "true" IP_FAMILY: "ipv4" OPENSTACK_BASTION_IMAGE_NAME: "cirros-0.6.1-x86_64-disk" OPENSTACK_BASTION_IMAGE_URL: https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/cirros/2022-12-05/cirros-0.6.1-x86_64-disk.img diff --git a/test/e2e/suites/e2e/autoscaler_test.go b/test/e2e/suites/e2e/autoscaler_test.go new file mode 100644 index 0000000000..0c59421f2b --- /dev/null +++ b/test/e2e/suites/e2e/autoscaler_test.go @@ -0,0 +1,54 @@ +//go:build e2e + +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + capi_e2e "sigs.k8s.io/cluster-api/test/e2e" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-openstack/test/e2e/shared" +) + +var _ = Describe("Autoscaler on ClusterClass cluster [Autoscaler] [ClusterClass]", func() { + capi_e2e.AutoscalerSpec(context.TODO(), func() capi_e2e.AutoscalerSpecInput { + infraAPIGroup := infrav1.GroupName + autoscalerFlavor := shared.FlavorTopology + + return capi_e2e.AutoscalerSpecInput{ + E2EConfig: e2eCtx.E2EConfig, + ClusterctlConfigPath: e2eCtx.Environment.ClusterctlConfigPath, + BootstrapClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + ArtifactFolder: e2eCtx.Settings.ArtifactFolder, + SkipCleanup: false, + Flavor: &autoscalerFlavor, + InfrastructureMachineTemplateKind: "openstackmachinetemplates", + // CAPO does not support machinePools + InfrastructureMachinePoolTemplateKind: "", + InfrastructureMachinePoolKind: "", + InfrastructureAPIGroup: infraAPIGroup, + AutoscalerVersion: e2eCtx.E2EConfig.MustGetVariable("AUTOSCALER_VERSION"), + InstallOnManagementCluster: false, + ScaleToAndFromZero: true, + PostNamespaceCreated: nil, + } + }) +})