Skip to content

Commit 3b560fa

Browse files
committed
Add compute host aggregates supported
Signed-off-by: Stephen Finucane <stephenfin@redhat.com> Generated-by: ./cmd/scaffold-controller
1 parent f93d141 commit 3b560fa

39 files changed

Lines changed: 1203 additions & 0 deletions
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2025 The ORC Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
// HostAggregateResourceSpec contains the desired state of the resource.
20+
type HostAggregateResourceSpec struct {
21+
// name will be the name of the created resource. If not specified, the
22+
// name of the ORC object will be used.
23+
// +optional
24+
Name *OpenStackName `json:"name,omitempty"`
25+
26+
// description is a human-readable description for the resource.
27+
// +kubebuilder:validation:MinLength:=1
28+
// +kubebuilder:validation:MaxLength:=255
29+
// +optional
30+
Description *string `json:"description,omitempty"`
31+
32+
// TODO(scaffolding): Add more types.
33+
// To see what is supported, you can take inspiration from the CreateOpts stucture from
34+
// github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates
35+
//
36+
// Until you have implemented mutability for the field, you must add a CEL validation
37+
// preventing the field being modified:
38+
// `// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="<fieldname> is immutable"`
39+
}
40+
41+
// HostAggregateFilter defines an existing resource by its properties
42+
// +kubebuilder:validation:MinProperties:=1
43+
type HostAggregateFilter struct {
44+
// name of the existing resource
45+
// +optional
46+
Name *OpenStackName `json:"name,omitempty"`
47+
48+
// description of the existing resource
49+
// +kubebuilder:validation:MinLength:=1
50+
// +kubebuilder:validation:MaxLength:=255
51+
// +optional
52+
Description *string `json:"description,omitempty"`
53+
54+
// TODO(scaffolding): Add more types.
55+
// To see what is supported, you can take inspiration from the ListOpts stucture from
56+
// github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates
57+
}
58+
59+
// HostAggregateResourceStatus represents the observed state of the resource.
60+
type HostAggregateResourceStatus struct {
61+
// name is a Human-readable name for the resource. Might not be unique.
62+
// +kubebuilder:validation:MaxLength=1024
63+
// +optional
64+
Name string `json:"name,omitempty"`
65+
66+
// description is a human-readable description for the resource.
67+
// +kubebuilder:validation:MaxLength=1024
68+
// +optional
69+
Description string `json:"description,omitempty"`
70+
71+
// TODO(scaffolding): Add more types.
72+
// To see what is supported, you can take inspiration from the HostAggregate stucture from
73+
// github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates
74+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
apiVersion: openstack.k-orc.cloud/v1alpha1
3+
kind: HostAggregate
4+
metadata:
5+
name: hostaggregate-sample
6+
spec:
7+
cloudCredentialsRef:
8+
# TODO(scaffolding): Use openstack-admin if the resouce needs admin credentials to be created
9+
cloudName: openstack
10+
secretName: openstack-clouds
11+
managementPolicy: managed
12+
resource:
13+
description: Sample HostAggregate
14+
# TODO(scaffolding): Add all fields the resource supports
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
Copyright 2025 The ORC Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hostaggregate
18+
19+
import (
20+
"context"
21+
"iter"
22+
23+
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates"
24+
corev1 "k8s.io/api/core/v1"
25+
"k8s.io/utils/ptr"
26+
ctrl "sigs.k8s.io/controller-runtime"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
29+
orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
30+
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
31+
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
32+
"github.com/k-orc/openstack-resource-controller/v2/internal/logging"
33+
"github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
34+
orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
35+
)
36+
37+
// OpenStack resource types
38+
type (
39+
osResourceT = aggregates.HostAggregate
40+
41+
createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT]
42+
deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT]
43+
resourceReconciler = interfaces.ResourceReconciler[orcObjectPT, osResourceT]
44+
helperFactory = interfaces.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT]
45+
)
46+
47+
type hostaggregateActuator struct {
48+
osClient osclients.HostAggregateClient
49+
k8sClient client.Client
50+
}
51+
52+
var _ createResourceActuator = hostaggregateActuator{}
53+
var _ deleteResourceActuator = hostaggregateActuator{}
54+
55+
func (hostaggregateActuator) GetResourceID(osResource *osResourceT) string {
56+
return osResource.ID
57+
}
58+
59+
func (actuator hostaggregateActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) {
60+
resource, err := actuator.osClient.GetHostAggregate(ctx, id)
61+
if err != nil {
62+
return nil, progress.WrapError(err)
63+
}
64+
return resource, nil
65+
}
66+
67+
func (actuator hostaggregateActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) {
68+
resourceSpec := orcObject.Spec.Resource
69+
if resourceSpec == nil {
70+
return nil, false
71+
}
72+
73+
// TODO(scaffolding) If you need to filter resources on fields that the List() function
74+
// of gophercloud does not support, it's possible to perform client-side filtering.
75+
// Check osclients.ResourceFilter
76+
77+
listOpts := aggregates.ListOpts{
78+
Name: getResourceName(orcObject),
79+
Description: ptr.Deref(resourceSpec.Description, ""),
80+
}
81+
82+
return actuator.osClient.ListHostAggregates(ctx, listOpts), true
83+
}
84+
85+
func (actuator hostaggregateActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
86+
// TODO(scaffolding) If you need to filter resources on fields that the List() function
87+
// of gophercloud does not support, it's possible to perform client-side filtering.
88+
// Check osclients.ResourceFilter
89+
90+
listOpts := aggregates.ListOpts{
91+
Name: string(ptr.Deref(filter.Name, "")),
92+
Description: string(ptr.Deref(filter.Description, "")),
93+
// TODO(scaffolding): Add more import filters
94+
}
95+
96+
return actuator.osClient.ListHostAggregates(ctx, listOpts), nil
97+
}
98+
99+
func (actuator hostaggregateActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) {
100+
resource := obj.Spec.Resource
101+
102+
if resource == nil {
103+
// Should have been caught by API validation
104+
return nil, progress.WrapError(
105+
orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set"))
106+
}
107+
createOpts := aggregates.CreateOpts{
108+
Name: getResourceName(obj),
109+
Description: ptr.Deref(resource.Description, ""),
110+
// TODO(scaffolding): Add more fields
111+
}
112+
113+
osResource, err := actuator.osClient.CreateHostAggregate(ctx, createOpts)
114+
if err != nil {
115+
// We should require the spec to be updated before retrying a create which returned a conflict
116+
if !orcerrors.IsRetryable(err) {
117+
err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration creating resource: "+err.Error(), err)
118+
}
119+
return nil, progress.WrapError(err)
120+
}
121+
122+
return osResource, nil
123+
}
124+
125+
func (actuator hostaggregateActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus {
126+
return progress.WrapError(actuator.osClient.DeleteHostAggregate(ctx, resource.ID))
127+
}
128+
129+
func (actuator hostaggregateActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
130+
log := ctrl.LoggerFrom(ctx)
131+
resource := obj.Spec.Resource
132+
if resource == nil {
133+
// Should have been caught by API validation
134+
return progress.WrapError(
135+
orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Update requested, but spec.resource is not set"))
136+
}
137+
138+
updateOpts := aggregates.UpdateOpts{}
139+
140+
handleNameUpdate(&updateOpts, obj, osResource)
141+
handleDescriptionUpdate(&updateOpts, resource, osResource)
142+
143+
// TODO(scaffolding): add handler for all fields supporting mutability
144+
145+
needsUpdate, err := needsUpdate(updateOpts)
146+
if err != nil {
147+
return progress.WrapError(
148+
orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err))
149+
}
150+
if !needsUpdate {
151+
log.V(logging.Debug).Info("No changes")
152+
return nil
153+
}
154+
155+
_, err = actuator.osClient.UpdateHostAggregate(ctx, osResource.ID, updateOpts)
156+
157+
// We should require the spec to be updated before retrying an update which returned a conflict
158+
if orcerrors.IsConflict(err) {
159+
err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)
160+
}
161+
162+
if err != nil {
163+
return progress.WrapError(err)
164+
}
165+
166+
return progress.NeedsRefresh()
167+
}
168+
169+
func needsUpdate(updateOpts aggregates.UpdateOpts) (bool, error) {
170+
updateOptsMap, err := updateOpts.ToHostAggregateUpdateMap()
171+
if err != nil {
172+
return false, err
173+
}
174+
175+
updateMap, ok := updateOptsMap["aggregate"].(map[string]any)
176+
if !ok {
177+
updateMap = make(map[string]any)
178+
}
179+
180+
return len(updateMap) > 0, nil
181+
}
182+
183+
func handleNameUpdate(updateOpts *aggregates.UpdateOpts, obj orcObjectPT, osResource *osResourceT) {
184+
name := getResourceName(obj)
185+
if osResource.Name != name {
186+
updateOpts.Name = &name
187+
}
188+
}
189+
190+
func handleDescriptionUpdate(updateOpts *aggregates.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) {
191+
description := ptr.Deref(resource.Description, "")
192+
if osResource.Description != description {
193+
updateOpts.Description = &description
194+
}
195+
}
196+
197+
func (actuator hostaggregateActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) {
198+
return []resourceReconciler{
199+
actuator.updateResource,
200+
}, nil
201+
}
202+
203+
type hostaggregateHelperFactory struct{}
204+
205+
var _ helperFactory = hostaggregateHelperFactory{}
206+
207+
func newActuator(ctx context.Context, orcObject *orcv1alpha1.HostAggregate, controller interfaces.ResourceController) (hostaggregateActuator, progress.ReconcileStatus) {
208+
log := ctrl.LoggerFrom(ctx)
209+
210+
// Ensure credential secrets exist and have our finalizer
211+
_, reconcileStatus := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true })
212+
if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
213+
return hostaggregateActuator{}, reconcileStatus
214+
}
215+
216+
clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject)
217+
if err != nil {
218+
return hostaggregateActuator{}, progress.WrapError(err)
219+
}
220+
osClient, err := clientScope.NewHostAggregateClient()
221+
if err != nil {
222+
return hostaggregateActuator{}, progress.WrapError(err)
223+
}
224+
225+
return hostaggregateActuator{
226+
osClient: osClient,
227+
k8sClient: controller.GetK8sClient(),
228+
}, nil
229+
}
230+
231+
func (hostaggregateHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI {
232+
return hostaggregateAdapter{obj}
233+
}
234+
235+
func (hostaggregateHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) {
236+
return newActuator(ctx, orcObject, controller)
237+
}
238+
239+
func (hostaggregateHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) {
240+
return newActuator(ctx, orcObject, controller)
241+
}

0 commit comments

Comments
 (0)