diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index da07b7eee..43e864a0a 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -36,7 +36,20 @@ jobs:
with:
enable_workaround_docker_io: 'false'
branch: ${{ matrix.openstack_version }}
- enabled_services: "openstack-cli-server"
+ enabled_services: "openstack-cli-server,octavia,o-api,o-cw,o-hm,o-hk,o-da"
+ conf_overrides: |
+ enable_plugin octavia https://github.com/openstack/octavia ${{ matrix.openstack_version }}
+ enable_plugin neutron https://github.com/openstack/neutron ${{ matrix.openstack_version }}
+
+ ${{ matrix.devstack_conf_overrides }}
+ - name: Calculate go version
+ id: vars
+ run: echo "go_version=$(make go-version)" >> $GITHUB_OUTPUT
+
+ - name: Set up Go
+ uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # tag=v6.1.0
+ with:
+ go-version: ${{ steps.vars.outputs.go_version }}
- name: Deploy a Kind Cluster
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab
diff --git a/PROJECT b/PROJECT
index 8d6e2c12d..d3f41ffce 100644
--- a/PROJECT
+++ b/PROJECT
@@ -56,6 +56,14 @@ resources:
kind: KeyPair
path: github.com/k-orc/openstack-resource-controller/api/v1alpha1
version: v1alpha1
+- api:
+ crdVersion: v1
+ namespaced: true
+ domain: k-orc.cloud
+ group: openstack
+ kind: LoadBalancer
+ path: github.com/k-orc/openstack-resource-controller/api/v1alpha1
+ version: v1alpha1
- api:
crdVersion: v1
namespaced: true
diff --git a/api/v1alpha1/loadbalancer_types.go b/api/v1alpha1/loadbalancer_types.go
new file mode 100644
index 000000000..d7bb4c1bc
--- /dev/null
+++ b/api/v1alpha1/loadbalancer_types.go
@@ -0,0 +1,252 @@
+/*
+Copyright 2026 The ORC 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 v1alpha1
+
+// Octavia provisioning status values
+const (
+ LoadbalancerProvisioningStatusActive = "ACTIVE"
+ LoadbalancerProvisioningStatusError = "ERROR"
+ LoadbalancerProvisioningStatusPendingCreate = "PENDING_CREATE"
+ LoadbalancerProvisioningStatusPendingUpdate = "PENDING_UPDATE"
+ LoadbalancerProvisioningStatusPendingDelete = "PENDING_DELETE"
+)
+
+// +kubebuilder:validation:MinLength:=1
+// +kubebuilder:validation:MaxLength:=255
+type LoadBalancerTag string
+
+// LoadBalancerResourceSpec contains the desired state of the resource.
+// +kubebuilder:validation:XValidation:rule="has(self.vipSubnetRef) || has(self.vipNetworkRef) || has(self.vipPortRef)",message="at least one of vipSubnetRef, vipNetworkRef, or vipPortRef must be specified"
+type LoadBalancerResourceSpec struct {
+ // name will be the name of the created resource. If not specified, the
+ // name of the ORC object will be used.
+ // +optional
+ Name *OpenStackName `json:"name,omitempty"`
+
+ // description is a human-readable description for the resource.
+ // +kubebuilder:validation:MinLength:=1
+ // +kubebuilder:validation:MaxLength:=255
+ // +optional
+ Description *string `json:"description,omitempty"`
+
+ // vipSubnetRef is the subnet on which to allocate the load balancer's address.
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="vipSubnetRef is immutable"
+ VipSubnetRef *KubernetesNameRef `json:"vipSubnetRef,omitempty"`
+
+ // vipNetworkRef is the network on which to allocate the load balancer's address.
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="vipNetworkRef is immutable"
+ VipNetworkRef *KubernetesNameRef `json:"vipNetworkRef,omitempty"`
+
+ // vipPortRef is a reference to a neutron port to use for the VIP. If the port
+ // has more than one subnet you must specify either vipSubnetRef or vipAddress
+ // to clarify which address should be used for the VIP.
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="vipPortRef is immutable"
+ VipPortRef *KubernetesNameRef `json:"vipPortRef,omitempty"`
+
+ // flavorRef is a reference to the ORC Flavor which this resource is associated with.
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="flavorRef is immutable"
+ FlavorRef *KubernetesNameRef `json:"flavorRef,omitempty"`
+
+ // projectRef is a reference to the ORC Project which this resource is associated with.
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable"
+ ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"`
+
+ // adminStateUp is the administrative state of the load balancer, which is up (true) or down (false)
+ // +optional
+ AdminStateUp *bool `json:"adminStateUp,omitempty"`
+
+ // availabilityZone is the availability zone in which to create the load balancer.
+ // +kubebuilder:validation:MaxLength=255
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="availabilityZone is immutable"
+ AvailabilityZone string `json:"availabilityZone,omitempty"`
+
+ // provider is the name of the load balancer provider.
+ // +kubebuilder:validation:MaxLength=255
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="provider is immutable"
+ Provider string `json:"provider,omitempty"`
+
+ // vipAddress is the specific IP address to use for the VIP (optional).
+ // If not specified, one is allocated automatically from the subnet.
+ // +optional
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="vipAddress is immutable"
+ VipAddress *IPvAny `json:"vipAddress,omitempty"`
+
+ // tags is a list of tags which will be applied to the load balancer.
+ // +kubebuilder:validation:MaxItems:=64
+ // +listType=set
+ // +optional
+ Tags []LoadBalancerTag `json:"tags,omitempty"`
+}
+
+// LoadBalancerFilter defines an existing resource by its properties
+// +kubebuilder:validation:MinProperties:=1
+type LoadBalancerFilter struct {
+ // name of the existing resource
+ // +optional
+ Name *OpenStackName `json:"name,omitempty"`
+
+ // description of the existing resource
+ // +kubebuilder:validation:MinLength:=1
+ // +kubebuilder:validation:MaxLength:=255
+ // +optional
+ Description *string `json:"description,omitempty"`
+
+ // projectRef is a reference to the ORC Project this resource is associated with.
+ // Typically, only used by admin.
+ // +optional
+ ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"`
+
+ // vipSubnetRef filters by the subnet on which the load balancer's address is allocated.
+ // +optional
+ VipSubnetRef *KubernetesNameRef `json:"vipSubnetRef,omitempty"`
+
+ // vipNetworkRef filters by the network on which the load balancer's address is allocated.
+ // +optional
+ VipNetworkRef *KubernetesNameRef `json:"vipNetworkRef,omitempty"`
+
+ // vipPortRef filters by the neutron port used for the VIP.
+ // +optional
+ VipPortRef *KubernetesNameRef `json:"vipPortRef,omitempty"`
+
+ // availabilityZone is the availability zone in which to create the load balancer.
+ // +kubebuilder:validation:MaxLength=255
+ // +optional
+ AvailabilityZone string `json:"availabilityZone,omitempty"`
+
+ // provider filters by the name of the load balancer provider.
+ // +kubebuilder:validation:MaxLength=255
+ // +optional
+ Provider string `json:"provider,omitempty"`
+
+ // vipAddress filters by the IP address of the load balancer's VIP.
+ // +kubebuilder:validation:MaxLength=64
+ // +optional
+ VipAddress string `json:"vipAddress,omitempty"`
+
+ // tags is a list of tags to filter by. If specified, the resource must
+ // have all of the tags specified to be included in the result.
+ // +listType=set
+ // +optional
+ // +kubebuilder:validation:MaxItems:=64
+ Tags []LoadBalancerTag `json:"tags,omitempty"`
+
+ // tagsAny is a list of tags to filter by. If specified, the resource
+ // must have at least one of the tags specified to be included in the
+ // result.
+ // +listType=set
+ // +optional
+ // +kubebuilder:validation:MaxItems:=64
+ TagsAny []LoadBalancerTag `json:"tagsAny,omitempty"`
+
+ // notTags is a list of tags to filter by. If specified, resources which
+ // contain all of the given tags will be excluded from the result.
+ // +listType=set
+ // +optional
+ // +kubebuilder:validation:MaxItems:=64
+ NotTags []LoadBalancerTag `json:"notTags,omitempty"`
+
+ // notTagsAny is a list of tags to filter by. If specified, resources
+ // which contain any of the given tags will be excluded from the result.
+ // +listType=set
+ // +optional
+ // +kubebuilder:validation:MaxItems:=64
+ NotTagsAny []LoadBalancerTag `json:"notTagsAny,omitempty"`
+}
+
+// LoadBalancerResourceStatus represents the observed state of the resource.
+type LoadBalancerResourceStatus struct {
+ // name is a Human-readable name for the resource. Might not be unique.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ Name string `json:"name,omitempty"`
+
+ // description is a human-readable description for the resource.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ Description string `json:"description,omitempty"`
+
+ // vipSubnetID is the ID of the Subnet to which the resource is associated.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ VipSubnetID string `json:"vipSubnetID,omitempty"`
+
+ // vipNetworkID is the ID of the Network to which the resource is associated.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ VipNetworkID string `json:"vipNetworkID,omitempty"`
+
+ // vipPortID is the ID of the Port to which the resource is associated.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ VipPortID string `json:"vipPortID,omitempty"`
+
+ // flavorID is the ID of the Flavor to which the resource is associated.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ FlavorID string `json:"flavorID,omitempty"`
+
+ // projectID is the ID of the Project to which the resource is associated.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ ProjectID string `json:"projectID,omitempty"`
+
+ // adminStateUp is the administrative state of the load balancer,
+ // which is up (true) or down (false).
+ // +optional
+ AdminStateUp *bool `json:"adminStateUp,omitempty"`
+
+ // tags is the list of tags on the resource.
+ // +listType=atomic
+ // +optional
+ // +kubebuilder:validation:MaxItems:=64
+ // +kubebuilder:validation:items:MaxLength=255
+ Tags []string `json:"tags,omitempty"`
+
+ // availabilityZone is the availability zone where the load balancer is located.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ AvailabilityZone string `json:"availabilityZone,omitempty"`
+
+ // provisioningStatus is the provisioning status of the load balancer.
+ // This value is ACTIVE, PENDING_CREATE or ERROR.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ ProvisioningStatus string `json:"provisioningStatus,omitempty"`
+
+ // operatingStatus is the operating status of the load balancer,
+ // such as ONLINE or OFFLINE.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ OperatingStatus string `json:"operatingStatus,omitempty"`
+
+ // provider is the name of the load balancer provider.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ Provider string `json:"provider,omitempty"`
+
+ // vipAddress is the IP address of the load balancer's VIP.
+ // +optional
+ // +kubebuilder:validation:MaxLength=64
+ VipAddress string `json:"vipAddress,omitempty"`
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 74fd9dcdf..7aab3cd10 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -1975,6 +1975,308 @@ func (in *KeyPairStatus) DeepCopy() *KeyPairStatus {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) {
+ *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 LoadBalancer.
+func (in *LoadBalancer) DeepCopy() *LoadBalancer {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancer)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *LoadBalancer) 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 *LoadBalancerFilter) DeepCopyInto(out *LoadBalancerFilter) {
+ *out = *in
+ if in.Name != nil {
+ in, out := &in.Name, &out.Name
+ *out = new(OpenStackName)
+ **out = **in
+ }
+ if in.Description != nil {
+ in, out := &in.Description, &out.Description
+ *out = new(string)
+ **out = **in
+ }
+ if in.ProjectRef != nil {
+ in, out := &in.ProjectRef, &out.ProjectRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.VipSubnetRef != nil {
+ in, out := &in.VipSubnetRef, &out.VipSubnetRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.VipNetworkRef != nil {
+ in, out := &in.VipNetworkRef, &out.VipNetworkRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.VipPortRef != nil {
+ in, out := &in.VipPortRef, &out.VipPortRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.Tags != nil {
+ in, out := &in.Tags, &out.Tags
+ *out = make([]LoadBalancerTag, len(*in))
+ copy(*out, *in)
+ }
+ if in.TagsAny != nil {
+ in, out := &in.TagsAny, &out.TagsAny
+ *out = make([]LoadBalancerTag, len(*in))
+ copy(*out, *in)
+ }
+ if in.NotTags != nil {
+ in, out := &in.NotTags, &out.NotTags
+ *out = make([]LoadBalancerTag, len(*in))
+ copy(*out, *in)
+ }
+ if in.NotTagsAny != nil {
+ in, out := &in.NotTagsAny, &out.NotTagsAny
+ *out = make([]LoadBalancerTag, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerFilter.
+func (in *LoadBalancerFilter) DeepCopy() *LoadBalancerFilter {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerFilter)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancerImport) DeepCopyInto(out *LoadBalancerImport) {
+ *out = *in
+ if in.ID != nil {
+ in, out := &in.ID, &out.ID
+ *out = new(string)
+ **out = **in
+ }
+ if in.Filter != nil {
+ in, out := &in.Filter, &out.Filter
+ *out = new(LoadBalancerFilter)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerImport.
+func (in *LoadBalancerImport) DeepCopy() *LoadBalancerImport {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerImport)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancerList) DeepCopyInto(out *LoadBalancerList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]LoadBalancer, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerList.
+func (in *LoadBalancerList) DeepCopy() *LoadBalancerList {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *LoadBalancerList) 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 *LoadBalancerResourceSpec) DeepCopyInto(out *LoadBalancerResourceSpec) {
+ *out = *in
+ if in.Name != nil {
+ in, out := &in.Name, &out.Name
+ *out = new(OpenStackName)
+ **out = **in
+ }
+ if in.Description != nil {
+ in, out := &in.Description, &out.Description
+ *out = new(string)
+ **out = **in
+ }
+ if in.VipSubnetRef != nil {
+ in, out := &in.VipSubnetRef, &out.VipSubnetRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.VipNetworkRef != nil {
+ in, out := &in.VipNetworkRef, &out.VipNetworkRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.VipPortRef != nil {
+ in, out := &in.VipPortRef, &out.VipPortRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.FlavorRef != nil {
+ in, out := &in.FlavorRef, &out.FlavorRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.ProjectRef != nil {
+ in, out := &in.ProjectRef, &out.ProjectRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.AdminStateUp != nil {
+ in, out := &in.AdminStateUp, &out.AdminStateUp
+ *out = new(bool)
+ **out = **in
+ }
+ if in.VipAddress != nil {
+ in, out := &in.VipAddress, &out.VipAddress
+ *out = new(IPvAny)
+ **out = **in
+ }
+ if in.Tags != nil {
+ in, out := &in.Tags, &out.Tags
+ *out = make([]LoadBalancerTag, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerResourceSpec.
+func (in *LoadBalancerResourceSpec) DeepCopy() *LoadBalancerResourceSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerResourceSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancerResourceStatus) DeepCopyInto(out *LoadBalancerResourceStatus) {
+ *out = *in
+ if in.AdminStateUp != nil {
+ in, out := &in.AdminStateUp, &out.AdminStateUp
+ *out = new(bool)
+ **out = **in
+ }
+ if in.Tags != nil {
+ in, out := &in.Tags, &out.Tags
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerResourceStatus.
+func (in *LoadBalancerResourceStatus) DeepCopy() *LoadBalancerResourceStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerResourceStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) {
+ *out = *in
+ if in.Import != nil {
+ in, out := &in.Import, &out.Import
+ *out = new(LoadBalancerImport)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Resource != nil {
+ in, out := &in.Resource, &out.Resource
+ *out = new(LoadBalancerResourceSpec)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ManagedOptions != nil {
+ in, out := &in.ManagedOptions, &out.ManagedOptions
+ *out = new(ManagedOptions)
+ **out = **in
+ }
+ out.CloudCredentialsRef = in.CloudCredentialsRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerSpec.
+func (in *LoadBalancerSpec) DeepCopy() *LoadBalancerSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LoadBalancerStatus) DeepCopyInto(out *LoadBalancerStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.ID != nil {
+ in, out := &in.ID, &out.ID
+ *out = new(string)
+ **out = **in
+ }
+ if in.Resource != nil {
+ in, out := &in.Resource, &out.Resource
+ *out = new(LoadBalancerResourceStatus)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerStatus.
+func (in *LoadBalancerStatus) DeepCopy() *LoadBalancerStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(LoadBalancerStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ManagedOptions) DeepCopyInto(out *ManagedOptions) {
*out = *in
diff --git a/api/v1alpha1/zz_generated.loadbalancer-resource.go b/api/v1alpha1/zz_generated.loadbalancer-resource.go
new file mode 100644
index 000000000..56a882684
--- /dev/null
+++ b/api/v1alpha1/zz_generated.loadbalancer-resource.go
@@ -0,0 +1,177 @@
+// Code generated by resource-generator. DO NOT EDIT.
+/*
+Copyright The ORC 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 v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// LoadBalancerImport specifies an existing resource which will be imported instead of
+// creating a new one
+// +kubebuilder:validation:MinProperties:=1
+// +kubebuilder:validation:MaxProperties:=1
+type LoadBalancerImport struct {
+ // id contains the unique identifier of an existing OpenStack resource. Note
+ // that when specifying an import by ID, the resource MUST already exist.
+ // The ORC object will enter an error state if the resource does not exist.
+ // +optional
+ // +kubebuilder:validation:Format:=uuid
+ ID *string `json:"id,omitempty"`
+
+ // filter contains a resource query which is expected to return a single
+ // result. The controller will continue to retry if filter returns no
+ // results. If filter returns multiple results the controller will set an
+ // error state and will not continue to retry.
+ // +optional
+ Filter *LoadBalancerFilter `json:"filter,omitempty"`
+}
+
+// LoadBalancerSpec defines the desired state of an ORC object.
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'managed' ? has(self.resource) : true",message="resource must be specified when policy is managed"
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'managed' ? !has(self.__import__) : true",message="import may not be specified when policy is managed"
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'unmanaged' ? !has(self.resource) : true",message="resource may not be specified when policy is unmanaged"
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'unmanaged' ? has(self.__import__) : true",message="import must be specified when policy is unmanaged"
+// +kubebuilder:validation:XValidation:rule="has(self.managedOptions) ? self.managementPolicy == 'managed' : true",message="managedOptions may only be provided when policy is managed"
+type LoadBalancerSpec struct {
+ // import refers to an existing OpenStack resource which will be imported instead of
+ // creating a new one.
+ // +optional
+ Import *LoadBalancerImport `json:"import,omitempty"`
+
+ // resource specifies the desired state of the resource.
+ //
+ // resource may not be specified if the management policy is `unmanaged`.
+ //
+ // resource must be specified if the management policy is `managed`.
+ // +optional
+ Resource *LoadBalancerResourceSpec `json:"resource,omitempty"`
+
+ // managementPolicy defines how ORC will treat the object. Valid values are
+ // `managed`: ORC will create, update, and delete the resource; `unmanaged`:
+ // ORC will import an existing resource, and will not apply updates to it or
+ // delete it.
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="managementPolicy is immutable"
+ // +kubebuilder:default:=managed
+ // +optional
+ ManagementPolicy ManagementPolicy `json:"managementPolicy,omitempty"`
+
+ // managedOptions specifies options which may be applied to managed objects.
+ // +optional
+ ManagedOptions *ManagedOptions `json:"managedOptions,omitempty"`
+
+ // cloudCredentialsRef points to a secret containing OpenStack credentials
+ // +required
+ CloudCredentialsRef CloudCredentialsReference `json:"cloudCredentialsRef"`
+}
+
+// LoadBalancerStatus defines the observed state of an ORC resource.
+type LoadBalancerStatus struct {
+ // conditions represents the observed status of the object.
+ // Known .status.conditions.type are: "Available", "Progressing"
+ //
+ // Available represents the availability of the OpenStack resource. If it is
+ // true then the resource is ready for use.
+ //
+ // Progressing indicates whether the controller is still attempting to
+ // reconcile the current state of the OpenStack resource to the desired
+ // state. Progressing will be False either because the desired state has
+ // been achieved, or because some terminal error prevents it from ever being
+ // achieved and the controller is no longer attempting to reconcile. If
+ // Progressing is True, an observer waiting on the resource should continue
+ // to wait.
+ //
+ // +kubebuilder:validation:MaxItems:=32
+ // +patchMergeKey=type
+ // +patchStrategy=merge
+ // +listType=map
+ // +listMapKey=type
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+
+ // id is the unique identifier of the OpenStack resource.
+ // +optional
+ ID *string `json:"id,omitempty"`
+
+ // resource contains the observed state of the OpenStack resource.
+ // +optional
+ Resource *LoadBalancerResourceStatus `json:"resource,omitempty"`
+}
+
+var _ ObjectWithConditions = &LoadBalancer{}
+
+func (i *LoadBalancer) GetConditions() []metav1.Condition {
+ return i.Status.Conditions
+}
+
+// +genclient
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:categories=openstack
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="Resource ID"
+// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type=='Available')].status",description="Availability status of resource"
+// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Progressing')].message",description="Message describing current progress status"
+
+// LoadBalancer is the Schema for an ORC resource.
+type LoadBalancer struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // metadata contains the object metadata
+ // +optional
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // spec specifies the desired state of the resource.
+ // +optional
+ Spec LoadBalancerSpec `json:"spec,omitempty"`
+
+ // status defines the observed state of the resource.
+ // +optional
+ Status LoadBalancerStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// LoadBalancerList contains a list of LoadBalancer.
+type LoadBalancerList struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // metadata contains the list metadata
+ // +optional
+ metav1.ListMeta `json:"metadata,omitempty"`
+
+ // items contains a list of LoadBalancer.
+ // +required
+ Items []LoadBalancer `json:"items"`
+}
+
+func (l *LoadBalancerList) GetItems() []LoadBalancer {
+ return l.Items
+}
+
+func init() {
+ SchemeBuilder.Register(&LoadBalancer{}, &LoadBalancerList{})
+}
+
+func (i *LoadBalancer) GetCloudCredentialsRef() (*string, *CloudCredentialsReference) {
+ if i == nil {
+ return nil, nil
+ }
+
+ return &i.Namespace, &i.Spec.CloudCredentialsRef
+}
+
+var _ CloudCredentialsRefProvider = &LoadBalancer{}
diff --git a/cmd/manager/main.go b/cmd/manager/main.go
index 293aa2bab..0b761fe98 100644
--- a/cmd/manager/main.go
+++ b/cmd/manager/main.go
@@ -34,6 +34,7 @@ import (
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/group"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/image"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/keypair"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/loadbalancer"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/network"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/port"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/project"
@@ -126,6 +127,7 @@ func main() {
keypair.New(scopeFactory),
group.New(scopeFactory),
role.New(scopeFactory),
+ loadbalancer.New(scopeFactory),
}
restConfig := ctrl.GetConfigOrDie()
diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go
index 3b90d275e..091c81187 100644
--- a/cmd/models-schema/zz_generated.openapi.go
+++ b/cmd/models-schema/zz_generated.openapi.go
@@ -100,6 +100,14 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.KeyPairResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairResourceStatus(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.KeyPairSpec": schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairSpec(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.KeyPairStatus": schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairStatus(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancer": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancer(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerFilter": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerFilter(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerImport": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerImport(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerList": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerList(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerResourceSpec(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerResourceStatus(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerSpec": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerSpec(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerStatus": schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerStatus(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions": schema_openstack_resource_controller_v2_api_v1alpha1_ManagedOptions(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Network": schema_openstack_resource_controller_v2_api_v1alpha1_Network(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NetworkFilter": schema_openstack_resource_controller_v2_api_v1alpha1_NetworkFilter(ref),
@@ -3761,6 +3769,625 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairStatus(ref comm
}
}
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancer(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancer is the Schema for an ORC resource.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ 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{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ 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{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Description: "metadata contains the object metadata",
+ Default: map[string]interface{}{},
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
+ },
+ },
+ "spec": {
+ SchemaProps: spec.SchemaProps{
+ Description: "spec specifies the desired state of the resource.",
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerSpec"),
+ },
+ },
+ "status": {
+ SchemaProps: spec.SchemaProps{
+ Description: "status defines the observed state of the resource.",
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerStatus"),
+ },
+ },
+ },
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerFilter defines an existing resource by its properties",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "name": {
+ SchemaProps: spec.SchemaProps{
+ Description: "name of the existing resource",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "description": {
+ SchemaProps: spec.SchemaProps{
+ Description: "description of the existing resource",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "projectRef is a reference to the ORC Project this resource is associated with. Typically, only used by admin.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipSubnetRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipSubnetRef filters by the subnet on which the load balancer's address is allocated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipNetworkRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipNetworkRef filters by the network on which the load balancer's address is allocated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipPortRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipPortRef filters by the neutron port used for the VIP.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "availabilityZone": {
+ SchemaProps: spec.SchemaProps{
+ Description: "availabilityZone is the availability zone in which to create the load balancer.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "provider": {
+ SchemaProps: spec.SchemaProps{
+ Description: "provider filters by the name of the load balancer provider.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipAddress": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipAddress filters by the IP address of the load balancer's VIP.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "tags": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "tags is a list of tags to filter by. If specified, the resource must have all of the tags specified to be included in the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "tagsAny": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "tagsAny is a list of tags to filter by. If specified, the resource must have at least one of the tags specified to be included in the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "notTags": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "notTags is a list of tags to filter by. If specified, resources which contain all of the given tags will be excluded from the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "notTagsAny": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "notTagsAny is a list of tags to filter by. If specified, resources which contain any of the given tags will be excluded from the result.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerImport(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerImport specifies an existing resource which will be imported instead of creating a new one",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "id": {
+ SchemaProps: spec.SchemaProps{
+ Description: "id contains the unique identifier of an existing OpenStack resource. Note that when specifying an import by ID, the resource MUST already exist. The ORC object will enter an error state if the resource does not exist.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "filter": {
+ SchemaProps: spec.SchemaProps{
+ Description: "filter contains a resource query which is expected to return a single result. The controller will continue to retry if filter returns no results. If filter returns multiple results the controller will set an error state and will not continue to retry.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerFilter"),
+ },
+ },
+ },
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerFilter"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerList(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerList contains a list of LoadBalancer.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ 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{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ 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{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Description: "metadata contains the list metadata",
+ Default: map[string]interface{}{},
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
+ },
+ },
+ "items": {
+ SchemaProps: spec.SchemaProps{
+ Description: "items contains a list of LoadBalancer.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancer"),
+ },
+ },
+ },
+ },
+ },
+ },
+ Required: []string{"items"},
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancer", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerResourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerResourceSpec contains the desired state of the resource.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "name": {
+ SchemaProps: spec.SchemaProps{
+ Description: "name will be the name of the created resource. If not specified, the name of the ORC object will be used.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "description": {
+ SchemaProps: spec.SchemaProps{
+ Description: "description is a human-readable description for the resource.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipSubnetRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipSubnetRef is the subnet on which to allocate the load balancer's address.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipNetworkRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipNetworkRef is the network on which to allocate the load balancer's address.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipPortRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipPortRef is a reference to a neutron port to use for the VIP. If the port has more than one subnet you must specify either vipSubnetRef or vipAddress to clarify which address should be used for the VIP.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "flavorRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "flavorRef is a reference to the ORC Flavor which this resource is associated with.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "projectRef is a reference to the ORC Project which this resource is associated with.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "adminStateUp": {
+ SchemaProps: spec.SchemaProps{
+ Description: "adminStateUp is the administrative state of the load balancer, which is up (true) or down (false)",
+ Type: []string{"boolean"},
+ Format: "",
+ },
+ },
+ "availabilityZone": {
+ SchemaProps: spec.SchemaProps{
+ Description: "availabilityZone is the availability zone in which to create the load balancer.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "provider": {
+ SchemaProps: spec.SchemaProps{
+ Description: "provider is the name of the load balancer provider.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipAddress": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipAddress is the specific IP address to use for the VIP (optional). If not specified, one is allocated automatically from the subnet.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "tags": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "set",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "tags is a list of tags which will be applied to the load balancer.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerResourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerResourceStatus represents the observed state of the resource.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "name": {
+ SchemaProps: spec.SchemaProps{
+ Description: "name is a Human-readable name for the resource. Might not be unique.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "description": {
+ SchemaProps: spec.SchemaProps{
+ Description: "description is a human-readable description for the resource.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipSubnetID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipSubnetID is the ID of the Subnet to which the resource is associated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipNetworkID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipNetworkID is the ID of the Network to which the resource is associated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipPortID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipPortID is the ID of the Port to which the resource is associated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "flavorID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "flavorID is the ID of the Flavor to which the resource is associated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "projectID is the ID of the Project to which the resource is associated.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "adminStateUp": {
+ SchemaProps: spec.SchemaProps{
+ Description: "adminStateUp is the administrative state of the load balancer, which is up (true) or down (false).",
+ Type: []string{"boolean"},
+ Format: "",
+ },
+ },
+ "tags": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-type": "atomic",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "tags is the list of tags on the resource.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ "availabilityZone": {
+ SchemaProps: spec.SchemaProps{
+ Description: "availabilityZone is the availability zone where the load balancer is located.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "provisioningStatus": {
+ SchemaProps: spec.SchemaProps{
+ Description: "provisioningStatus is the provisioning status of the load balancer. This value is ACTIVE, PENDING_CREATE or ERROR.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "operatingStatus": {
+ SchemaProps: spec.SchemaProps{
+ Description: "operatingStatus is the operating status of the load balancer, such as ONLINE or OFFLINE.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "provider": {
+ SchemaProps: spec.SchemaProps{
+ Description: "provider is the name of the load balancer provider.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "vipAddress": {
+ SchemaProps: spec.SchemaProps{
+ Description: "vipAddress is the IP address of the load balancer's VIP.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerSpec defines the desired state of an ORC object.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "import": {
+ SchemaProps: spec.SchemaProps{
+ Description: "import refers to an existing OpenStack resource which will be imported instead of creating a new one.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerImport"),
+ },
+ },
+ "resource": {
+ SchemaProps: spec.SchemaProps{
+ Description: "resource specifies the desired state of the resource.\n\nresource may not be specified if the management policy is `unmanaged`.\n\nresource must be specified if the management policy is `managed`.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerResourceSpec"),
+ },
+ },
+ "managementPolicy": {
+ SchemaProps: spec.SchemaProps{
+ Description: "managementPolicy defines how ORC will treat the object. Valid values are `managed`: ORC will create, update, and delete the resource; `unmanaged`: ORC will import an existing resource, and will not apply updates to it or delete it.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "managedOptions": {
+ SchemaProps: spec.SchemaProps{
+ Description: "managedOptions specifies options which may be applied to managed objects.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions"),
+ },
+ },
+ "cloudCredentialsRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "cloudCredentialsRef points to a secret containing OpenStack credentials",
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference"),
+ },
+ },
+ },
+ Required: []string{"cloudCredentialsRef"},
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerImport", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerResourceSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_LoadBalancerStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "LoadBalancerStatus defines the observed state of an ORC resource.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "conditions": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-map-keys": []interface{}{
+ "type",
+ },
+ "x-kubernetes-list-type": "map",
+ "x-kubernetes-patch-merge-key": "type",
+ "x-kubernetes-patch-strategy": "merge",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "conditions represents the observed status of the object. Known .status.conditions.type are: \"Available\", \"Progressing\"\n\nAvailable represents the availability of the OpenStack resource. If it is true then the resource is ready for use.\n\nProgressing indicates whether the controller is still attempting to reconcile the current state of the OpenStack resource to the desired state. Progressing will be False either because the desired state has been achieved, or because some terminal error prevents it from ever being achieved and the controller is no longer attempting to reconcile. If Progressing is True, an observer waiting on the resource should continue to wait.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
+ },
+ },
+ },
+ },
+ },
+ "id": {
+ SchemaProps: spec.SchemaProps{
+ Description: "id is the unique identifier of the OpenStack resource.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "resource": {
+ SchemaProps: spec.SchemaProps{
+ Description: "resource contains the observed state of the OpenStack resource.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerResourceStatus"),
+ },
+ },
+ },
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LoadBalancerResourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"},
+ }
+}
+
func schema_openstack_resource_controller_v2_api_v1alpha1_ManagedOptions(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
diff --git a/cmd/resource-generator/main.go b/cmd/resource-generator/main.go
index b3f4ece76..ecd71a16a 100644
--- a/cmd/resource-generator/main.go
+++ b/cmd/resource-generator/main.go
@@ -160,6 +160,9 @@ var resources []templateFields = []templateFields{
{
Name: "Group",
},
+ {
+ Name: "LoadBalancer",
+ },
}
// These resources won't be generated
diff --git a/config/crd/bases/openstack.k-orc.cloud_loadbalancers.yaml b/config/crd/bases/openstack.k-orc.cloud_loadbalancers.yaml
new file mode 100644
index 000000000..08f8470e2
--- /dev/null
+++ b/config/crd/bases/openstack.k-orc.cloud_loadbalancers.yaml
@@ -0,0 +1,524 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.17.1
+ name: loadbalancers.openstack.k-orc.cloud
+spec:
+ group: openstack.k-orc.cloud
+ names:
+ categories:
+ - openstack
+ kind: LoadBalancer
+ listKind: LoadBalancerList
+ plural: loadbalancers
+ singular: loadbalancer
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - description: Resource ID
+ jsonPath: .status.id
+ name: ID
+ type: string
+ - description: Availability status of resource
+ jsonPath: .status.conditions[?(@.type=='Available')].status
+ name: Available
+ type: string
+ - description: Message describing current progress status
+ jsonPath: .status.conditions[?(@.type=='Progressing')].message
+ name: Message
+ type: string
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: LoadBalancer is the Schema for an ORC resource.
+ 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: spec specifies the desired state of the resource.
+ properties:
+ cloudCredentialsRef:
+ description: cloudCredentialsRef points to a secret containing OpenStack
+ credentials
+ properties:
+ cloudName:
+ description: cloudName specifies the name of the entry in the
+ clouds.yaml file to use.
+ maxLength: 256
+ minLength: 1
+ type: string
+ secretName:
+ description: |-
+ secretName is the name of a secret in the same namespace as the resource being provisioned.
+ The secret must contain a key named `clouds.yaml` which contains an OpenStack clouds.yaml file.
+ The secret may optionally contain a key named `cacert` containing a PEM-encoded CA certificate.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - cloudName
+ - secretName
+ type: object
+ import:
+ description: |-
+ import refers to an existing OpenStack resource which will be imported instead of
+ creating a new one.
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ filter:
+ description: |-
+ filter contains a resource query which is expected to return a single
+ result. The controller will continue to retry if filter returns no
+ results. If filter returns multiple results the controller will set an
+ error state and will not continue to retry.
+ minProperties: 1
+ properties:
+ availabilityZone:
+ description: availabilityZone is the availability zone in
+ which to create the load balancer.
+ maxLength: 255
+ type: string
+ description:
+ description: description of the existing resource
+ maxLength: 255
+ minLength: 1
+ type: string
+ name:
+ description: name of the existing resource
+ maxLength: 255
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ notTags:
+ description: |-
+ notTags is a list of tags to filter by. If specified, resources which
+ contain all of the given tags will be excluded from the result.
+ items:
+ maxLength: 255
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-list-type: set
+ notTagsAny:
+ description: |-
+ notTagsAny is a list of tags to filter by. If specified, resources
+ which contain any of the given tags will be excluded from the result.
+ items:
+ maxLength: 255
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-list-type: set
+ projectRef:
+ description: |-
+ projectRef is a reference to the ORC Project this resource is associated with.
+ Typically, only used by admin.
+ maxLength: 253
+ minLength: 1
+ type: string
+ provider:
+ description: provider filters by the name of the load balancer
+ provider.
+ maxLength: 255
+ type: string
+ tags:
+ description: |-
+ tags is a list of tags to filter by. If specified, the resource must
+ have all of the tags specified to be included in the result.
+ items:
+ maxLength: 255
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-list-type: set
+ tagsAny:
+ description: |-
+ tagsAny is a list of tags to filter by. If specified, the resource
+ must have at least one of the tags specified to be included in the
+ result.
+ items:
+ maxLength: 255
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-list-type: set
+ vipAddress:
+ description: vipAddress filters by the IP address of the load
+ balancer's VIP.
+ maxLength: 64
+ type: string
+ vipNetworkRef:
+ description: vipNetworkRef filters by the network on which
+ the load balancer's address is allocated.
+ maxLength: 253
+ minLength: 1
+ type: string
+ vipPortRef:
+ description: vipPortRef filters by the neutron port used for
+ the VIP.
+ maxLength: 253
+ minLength: 1
+ type: string
+ vipSubnetRef:
+ description: vipSubnetRef filters by the subnet on which the
+ load balancer's address is allocated.
+ maxLength: 253
+ minLength: 1
+ type: string
+ type: object
+ id:
+ description: |-
+ id contains the unique identifier of an existing OpenStack resource. Note
+ that when specifying an import by ID, the resource MUST already exist.
+ The ORC object will enter an error state if the resource does not exist.
+ format: uuid
+ type: string
+ type: object
+ managedOptions:
+ description: managedOptions specifies options which may be applied
+ to managed objects.
+ properties:
+ onDelete:
+ default: delete
+ description: |-
+ onDelete specifies the behaviour of the controller when the ORC
+ object is deleted. Options are `delete` - delete the OpenStack resource;
+ `detach` - do not delete the OpenStack resource. If not specified, the
+ default is `delete`.
+ enum:
+ - delete
+ - detach
+ type: string
+ type: object
+ managementPolicy:
+ default: managed
+ description: |-
+ managementPolicy defines how ORC will treat the object. Valid values are
+ `managed`: ORC will create, update, and delete the resource; `unmanaged`:
+ ORC will import an existing resource, and will not apply updates to it or
+ delete it.
+ enum:
+ - managed
+ - unmanaged
+ type: string
+ x-kubernetes-validations:
+ - message: managementPolicy is immutable
+ rule: self == oldSelf
+ resource:
+ description: |-
+ resource specifies the desired state of the resource.
+
+ resource may not be specified if the management policy is `unmanaged`.
+
+ resource must be specified if the management policy is `managed`.
+ properties:
+ adminStateUp:
+ description: adminStateUp is the administrative state of the load
+ balancer, which is up (true) or down (false)
+ type: boolean
+ availabilityZone:
+ description: availabilityZone is the availability zone in which
+ to create the load balancer.
+ maxLength: 255
+ type: string
+ x-kubernetes-validations:
+ - message: availabilityZone is immutable
+ rule: self == oldSelf
+ description:
+ description: description is a human-readable description for the
+ resource.
+ maxLength: 255
+ minLength: 1
+ type: string
+ flavorRef:
+ description: flavorRef is a reference to the ORC Flavor which
+ this resource is associated with.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: flavorRef is immutable
+ rule: self == oldSelf
+ name:
+ description: |-
+ name will be the name of the created resource. If not specified, the
+ name of the ORC object will be used.
+ maxLength: 255
+ minLength: 1
+ pattern: ^[^,]+$
+ type: string
+ projectRef:
+ description: projectRef is a reference to the ORC Project which
+ this resource is associated with.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: projectRef is immutable
+ rule: self == oldSelf
+ provider:
+ description: provider is the name of the load balancer provider.
+ maxLength: 255
+ type: string
+ x-kubernetes-validations:
+ - message: provider is immutable
+ rule: self == oldSelf
+ tags:
+ description: tags is a list of tags which will be applied to the
+ load balancer.
+ items:
+ maxLength: 255
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-list-type: set
+ vipAddress:
+ description: |-
+ vipAddress is the specific IP address to use for the VIP (optional).
+ If not specified, one is allocated automatically from the subnet.
+ maxLength: 45
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: vipAddress is immutable
+ rule: self == oldSelf
+ vipNetworkRef:
+ description: vipNetworkRef is the network on which to allocate
+ the load balancer's address.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: vipNetworkRef is immutable
+ rule: self == oldSelf
+ vipPortRef:
+ description: |-
+ vipPortRef is a reference to a neutron port to use for the VIP. If the port
+ has more than one subnet you must specify either vipSubnetRef or vipAddress
+ to clarify which address should be used for the VIP.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: vipPortRef is immutable
+ rule: self == oldSelf
+ vipSubnetRef:
+ description: vipSubnetRef is the subnet on which to allocate the
+ load balancer's address.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: vipSubnetRef is immutable
+ rule: self == oldSelf
+ type: object
+ x-kubernetes-validations:
+ - message: at least one of vipSubnetRef, vipNetworkRef, or vipPortRef
+ must be specified
+ rule: has(self.vipSubnetRef) || has(self.vipNetworkRef) || has(self.vipPortRef)
+ required:
+ - cloudCredentialsRef
+ type: object
+ x-kubernetes-validations:
+ - message: resource must be specified when policy is managed
+ rule: 'self.managementPolicy == ''managed'' ? has(self.resource) : true'
+ - message: import may not be specified when policy is managed
+ rule: 'self.managementPolicy == ''managed'' ? !has(self.__import__)
+ : true'
+ - message: resource may not be specified when policy is unmanaged
+ rule: 'self.managementPolicy == ''unmanaged'' ? !has(self.resource)
+ : true'
+ - message: import must be specified when policy is unmanaged
+ rule: 'self.managementPolicy == ''unmanaged'' ? has(self.__import__)
+ : true'
+ - message: managedOptions may only be provided when policy is managed
+ rule: 'has(self.managedOptions) ? self.managementPolicy == ''managed''
+ : true'
+ status:
+ description: status defines the observed state of the resource.
+ properties:
+ conditions:
+ description: |-
+ conditions represents the observed status of the object.
+ Known .status.conditions.type are: "Available", "Progressing"
+
+ Available represents the availability of the OpenStack resource. If it is
+ true then the resource is ready for use.
+
+ Progressing indicates whether the controller is still attempting to
+ reconcile the current state of the OpenStack resource to the desired
+ state. Progressing will be False either because the desired state has
+ been achieved, or because some terminal error prevents it from ever being
+ achieved and the controller is no longer attempting to reconcile. If
+ Progressing is True, an observer waiting on the resource should continue
+ to wait.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ 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 may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ maxItems: 32
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ id:
+ description: id is the unique identifier of the OpenStack resource.
+ type: string
+ resource:
+ description: resource contains the observed state of the OpenStack
+ resource.
+ properties:
+ adminStateUp:
+ description: |-
+ adminStateUp is the administrative state of the load balancer,
+ which is up (true) or down (false).
+ type: boolean
+ availabilityZone:
+ description: availabilityZone is the availability zone where the
+ load balancer is located.
+ maxLength: 1024
+ type: string
+ description:
+ description: description is a human-readable description for the
+ resource.
+ maxLength: 1024
+ type: string
+ flavorID:
+ description: flavorID is the ID of the Flavor to which the resource
+ is associated.
+ maxLength: 1024
+ type: string
+ name:
+ description: name is a Human-readable name for the resource. Might
+ not be unique.
+ maxLength: 1024
+ type: string
+ operatingStatus:
+ description: |-
+ operatingStatus is the operating status of the load balancer,
+ such as ONLINE or OFFLINE.
+ maxLength: 1024
+ type: string
+ projectID:
+ description: projectID is the ID of the Project to which the resource
+ is associated.
+ maxLength: 1024
+ type: string
+ provider:
+ description: provider is the name of the load balancer provider.
+ maxLength: 1024
+ type: string
+ provisioningStatus:
+ description: |-
+ provisioningStatus is the provisioning status of the load balancer.
+ This value is ACTIVE, PENDING_CREATE or ERROR.
+ maxLength: 1024
+ type: string
+ tags:
+ description: tags is the list of tags on the resource.
+ items:
+ maxLength: 255
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-list-type: atomic
+ vipAddress:
+ description: vipAddress is the IP address of the load balancer's
+ VIP.
+ maxLength: 64
+ type: string
+ vipNetworkID:
+ description: vipNetworkID is the ID of the Network to which the
+ resource is associated.
+ maxLength: 1024
+ type: string
+ vipPortID:
+ description: vipPortID is the ID of the Port to which the resource
+ is associated.
+ maxLength: 1024
+ type: string
+ vipSubnetID:
+ description: vipSubnetID is the ID of the Subnet to which the
+ resource is associated.
+ maxLength: 1024
+ type: string
+ type: object
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 33b8c85e2..8012052f5 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -9,6 +9,7 @@ resources:
- bases/openstack.k-orc.cloud_groups.yaml
- bases/openstack.k-orc.cloud_images.yaml
- bases/openstack.k-orc.cloud_keypairs.yaml
+- bases/openstack.k-orc.cloud_loadbalancers.yaml
- bases/openstack.k-orc.cloud_networks.yaml
- bases/openstack.k-orc.cloud_ports.yaml
- bases/openstack.k-orc.cloud_projects.yaml
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 5a0a7443b..047bace25 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -23,6 +23,7 @@ rules:
- groups
- images
- keypairs
+ - loadbalancers
- networks
- ports
- projects
@@ -53,6 +54,7 @@ rules:
- groups/status
- images/status
- keypairs/status
+ - loadbalancers/status
- networks/status
- ports/status
- projects/status
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index dac467c69..075c016fa 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -7,6 +7,7 @@ resources:
- openstack_v1alpha1_group.yaml
- openstack_v1alpha1_image.yaml
- openstack_v1alpha1_keypair.yaml
+- openstack_v1alpha1_loadbalancer.yaml
- openstack_v1alpha1_network.yaml
- openstack_v1alpha1_port.yaml
- openstack_v1alpha1_project.yaml
diff --git a/config/samples/openstack_v1alpha1_loadbalancer.yaml b/config/samples/openstack_v1alpha1_loadbalancer.yaml
new file mode 100644
index 000000000..9510e2d0f
--- /dev/null
+++ b/config/samples/openstack_v1alpha1_loadbalancer.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-sample
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: my-loadbalancer
+ description: Sample LoadBalancer
+ vipSubnetRef: loadbalancer-sample-subnet
+ vipNetworkRef: loadbalancer-sample-network
+ vipPortRef: loadbalancer-sample-port
+ vipAddress: 10.0.0.100
+ projectRef: loadbalancer-sample-project
+ adminStateUp: true
+ availabilityZone: nova
+ provider: amphora
+ tags:
+ - environment:production
+ - team:platform
diff --git a/internal/controllers/loadbalancer/actuator.go b/internal/controllers/loadbalancer/actuator.go
new file mode 100644
index 000000000..dc240b281
--- /dev/null
+++ b/internal/controllers/loadbalancer/actuator.go
@@ -0,0 +1,405 @@
+/*
+Copyright 2026 The ORC 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 loadbalancer
+
+import (
+ "context"
+ "iter"
+ "slices"
+ "time"
+
+ "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/utils/ptr"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/logging"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
+ orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
+)
+
+const (
+ // The frequency to poll when waiting for the load balancer to become ACTIVE
+ loadbalancerActivePollingPeriod = 15 * time.Second
+
+ // The frequency to poll when waiting for the load balancer to be deleted
+ loadbalancerDeletingPollingPeriod = 15 * time.Second
+)
+
+// OpenStack resource types
+type (
+ osResourceT = loadbalancers.LoadBalancer
+
+ createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT]
+ deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT]
+ resourceReconciler = interfaces.ResourceReconciler[orcObjectPT, osResourceT]
+ helperFactory = interfaces.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT]
+)
+
+type loadbalancerActuator struct {
+ osClient osclients.LoadBalancerClient
+ k8sClient client.Client
+}
+
+var _ createResourceActuator = loadbalancerActuator{}
+var _ deleteResourceActuator = loadbalancerActuator{}
+
+func (loadbalancerActuator) GetResourceID(osResource *osResourceT) string {
+ return osResource.ID
+}
+
+func (actuator loadbalancerActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) {
+ resource, err := actuator.osClient.GetLoadBalancer(ctx, id)
+ if err != nil {
+ return nil, progress.WrapError(err)
+ }
+ return resource, nil
+}
+
+func (actuator loadbalancerActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) {
+ resourceSpec := orcObject.Spec.Resource
+ if resourceSpec == nil {
+ return nil, false
+ }
+
+ listOpts := loadbalancers.ListOpts{
+ Name: getResourceName(orcObject),
+ }
+
+ return actuator.osClient.ListLoadBalancers(ctx, listOpts), true
+}
+
+func (actuator loadbalancerActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
+ var reconcileStatus progress.ReconcileStatus
+
+ vipNetwork, rs := dependency.FetchDependency[*orcv1alpha1.Network, orcv1alpha1.Network](
+ ctx, actuator.k8sClient, obj.Namespace,
+ filter.VipNetworkRef, "Network",
+ func(n *orcv1alpha1.Network) bool { return orcv1alpha1.IsAvailable(n) && n.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+
+ project, rs := dependency.FetchDependency[*orcv1alpha1.Project, orcv1alpha1.Project](
+ ctx, actuator.k8sClient, obj.Namespace,
+ filter.ProjectRef, "Project",
+ func(n *orcv1alpha1.Project) bool { return orcv1alpha1.IsAvailable(n) && n.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+
+ vipSubnet, rs := dependency.FetchDependency[*orcv1alpha1.Subnet, orcv1alpha1.Subnet](
+ ctx, actuator.k8sClient, obj.Namespace,
+ filter.VipSubnetRef, "Subnet",
+ func(n *orcv1alpha1.Subnet) bool { return orcv1alpha1.IsAvailable(n) && n.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+
+ vipPort, rs := dependency.FetchDependency[*orcv1alpha1.Port, orcv1alpha1.Port](
+ ctx, actuator.k8sClient, obj.Namespace,
+ filter.VipPortRef, "Port",
+ func(n *orcv1alpha1.Port) bool { return orcv1alpha1.IsAvailable(n) && n.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return nil, reconcileStatus
+ }
+
+ listOpts := loadbalancers.ListOpts{
+ Name: string(ptr.Deref(filter.Name, "")),
+ Description: ptr.Deref(filter.Description, ""),
+ AvailabilityZone: filter.AvailabilityZone,
+ Provider: filter.Provider,
+ VipAddress: filter.VipAddress,
+ VipNetworkID: ptr.Deref(vipNetwork.Status.ID, ""),
+ ProjectID: ptr.Deref(project.Status.ID, ""),
+ VipSubnetID: ptr.Deref(vipSubnet.Status.ID, ""),
+ VipPortID: ptr.Deref(vipPort.Status.ID, ""),
+ }
+
+ return actuator.osClient.ListLoadBalancers(ctx, listOpts), reconcileStatus
+}
+
+func (actuator loadbalancerActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) {
+ resource := obj.Spec.Resource
+
+ if resource == nil {
+ // Should have been caught by API validation
+ return nil, progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set"))
+ }
+ var reconcileStatus progress.ReconcileStatus
+
+ var vipSubnetID string
+ if resource.VipSubnetRef != nil {
+ subnet, subnetDepRS := subnetDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Subnet) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(subnetDepRS)
+ if subnet != nil {
+ vipSubnetID = ptr.Deref(subnet.Status.ID, "")
+ }
+ }
+
+ var vipNetworkID string
+ if resource.VipNetworkRef != nil {
+ network, networkDepRS := networkDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Network) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(networkDepRS)
+ if network != nil {
+ vipNetworkID = ptr.Deref(network.Status.ID, "")
+ }
+ }
+
+ var vipPortID string
+ if resource.VipPortRef != nil {
+ port, portDepRS := portDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Port) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(portDepRS)
+ if port != nil {
+ vipPortID = ptr.Deref(port.Status.ID, "")
+ }
+ }
+
+ var flavorID string
+ if resource.FlavorRef != nil {
+ flavor, flavorDepRS := flavorDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Flavor) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(flavorDepRS)
+ if flavor != nil {
+ flavorID = ptr.Deref(flavor.Status.ID, "")
+ }
+ }
+
+ var projectID string
+ if resource.ProjectRef != nil {
+ project, projectDepRS := projectDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Project) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(projectDepRS)
+ if project != nil {
+ projectID = ptr.Deref(project.Status.ID, "")
+ }
+ }
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return nil, reconcileStatus
+ }
+
+ tags := make([]string, len(resource.Tags))
+ for i := range resource.Tags {
+ tags[i] = string(resource.Tags[i])
+ }
+ // Sort tags before creation to simplify comparisons
+ slices.Sort(tags)
+
+ createOpts := loadbalancers.CreateOpts{
+ Name: getResourceName(obj),
+ Description: ptr.Deref(resource.Description, ""),
+ VipSubnetID: vipSubnetID,
+ VipNetworkID: vipNetworkID,
+ VipPortID: vipPortID,
+ FlavorID: flavorID,
+ ProjectID: projectID,
+ AdminStateUp: resource.AdminStateUp,
+ AvailabilityZone: resource.AvailabilityZone,
+ Provider: resource.Provider,
+ VipAddress: string(ptr.Deref(resource.VipAddress, "")),
+ Tags: tags,
+ }
+
+ osResource, err := actuator.osClient.CreateLoadBalancer(ctx, createOpts)
+ if err != nil {
+ // We should require the spec to be updated before retrying a create which returned a conflict
+ if !orcerrors.IsRetryable(err) {
+ err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration creating resource: "+err.Error(), err)
+ }
+ return nil, progress.WrapError(err)
+ }
+
+ return osResource, nil
+}
+
+func (actuator loadbalancerActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus {
+ switch resource.ProvisioningStatus {
+ case orcv1alpha1.LoadbalancerProvisioningStatusPendingDelete:
+ return progress.WaitingOnOpenStack(progress.WaitingOnReady, loadbalancerDeletingPollingPeriod)
+ case orcv1alpha1.LoadbalancerProvisioningStatusPendingCreate, orcv1alpha1.LoadbalancerProvisioningStatusPendingUpdate:
+ // We can't delete a loadbalancer that's in a pending state, so we need to wait for it to become ACTIVE
+ return progress.WaitingOnOpenStack(progress.WaitingOnReady, loadbalancerActivePollingPeriod)
+ }
+
+ err := actuator.osClient.DeleteLoadBalancer(ctx, resource.ID)
+ // 409 Conflict means the loadbalancer is already in PENDING_DELETE state.
+ // Treat this as success and let the controller poll for deletion completion.
+ if orcerrors.IsConflict(err) {
+ return progress.WaitingOnOpenStack(progress.WaitingOnReady, loadbalancerDeletingPollingPeriod)
+ }
+ return progress.WrapError(err)
+}
+
+func (actuator loadbalancerActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
+ log := ctrl.LoggerFrom(ctx)
+ resource := obj.Spec.Resource
+ if resource == nil {
+ // Should have been caught by API validation
+ return progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Update requested, but spec.resource is not set"))
+ }
+
+ updateOpts := loadbalancers.UpdateOpts{}
+
+ handleNameUpdate(&updateOpts, obj, osResource)
+ handleDescriptionUpdate(&updateOpts, resource, osResource)
+ handleAdminStateUpdate(&updateOpts, resource, osResource)
+ handleTagsUpdate(&updateOpts, resource, osResource)
+
+ needsUpdate, err := needsUpdate(updateOpts)
+ if err != nil {
+ return progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err))
+ }
+ if !needsUpdate {
+ log.V(logging.Debug).Info("No changes")
+ return nil
+ }
+
+ _, err = actuator.osClient.UpdateLoadBalancer(ctx, osResource.ID, updateOpts)
+
+ // We should require the spec to be updated before retrying an update which returned a conflict
+ if orcerrors.IsConflict(err) {
+ err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)
+ }
+
+ if err != nil {
+ return progress.WrapError(err)
+ }
+
+ return progress.NeedsRefresh()
+}
+
+func needsUpdate(updateOpts loadbalancers.UpdateOpts) (bool, error) {
+ updateOptsMap, err := updateOpts.ToLoadBalancerUpdateMap()
+ if err != nil {
+ return false, err
+ }
+
+ updateMap, ok := updateOptsMap["loadbalancer"].(map[string]any)
+ if !ok {
+ updateMap = make(map[string]any)
+ }
+
+ return len(updateMap) > 0, nil
+}
+
+func handleNameUpdate(updateOpts *loadbalancers.UpdateOpts, obj orcObjectPT, osResource *osResourceT) {
+ name := getResourceName(obj)
+ if osResource.Name != name {
+ updateOpts.Name = &name
+ }
+}
+
+func handleDescriptionUpdate(updateOpts *loadbalancers.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) {
+ description := ptr.Deref(resource.Description, "")
+ if osResource.Description != description {
+ updateOpts.Description = &description
+ }
+}
+
+func handleAdminStateUpdate(updateOpts *loadbalancers.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) {
+ // Default to true if not specified (OpenStack default)
+ adminStateUp := ptr.Deref(resource.AdminStateUp, true)
+ if osResource.AdminStateUp != adminStateUp {
+ updateOpts.AdminStateUp = &adminStateUp
+ }
+}
+
+func handleTagsUpdate(updateOpts *loadbalancers.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) {
+ desiredTags := make([]string, len(resource.Tags))
+ for i, tag := range resource.Tags {
+ desiredTags[i] = string(tag)
+ }
+
+ slices.Sort(desiredTags)
+ slices.Sort(osResource.Tags)
+
+ if !slices.Equal(desiredTags, osResource.Tags) {
+ updateOpts.Tags = &desiredTags
+ }
+}
+
+func (actuator loadbalancerActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) {
+ return []resourceReconciler{
+ actuator.updateResource,
+ }, nil
+}
+
+type loadbalancerHelperFactory struct{}
+
+var _ helperFactory = loadbalancerHelperFactory{}
+
+func newActuator(ctx context.Context, orcObject *orcv1alpha1.LoadBalancer, controller interfaces.ResourceController) (loadbalancerActuator, progress.ReconcileStatus) {
+ log := ctrl.LoggerFrom(ctx)
+
+ // Ensure credential secrets exist and have our finalizer
+ _, reconcileStatus := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true })
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return loadbalancerActuator{}, reconcileStatus
+ }
+
+ clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject)
+ if err != nil {
+ return loadbalancerActuator{}, progress.WrapError(err)
+ }
+ osClient, err := clientScope.NewLoadBalancerClient()
+ if err != nil {
+ return loadbalancerActuator{}, progress.WrapError(err)
+ }
+
+ return loadbalancerActuator{
+ osClient: osClient,
+ k8sClient: controller.GetK8sClient(),
+ }, nil
+}
+
+func (loadbalancerHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI {
+ return loadbalancerAdapter{obj}
+}
+
+func (loadbalancerHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) {
+ return newActuator(ctx, orcObject, controller)
+}
+
+func (loadbalancerHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) {
+ return newActuator(ctx, orcObject, controller)
+}
diff --git a/internal/controllers/loadbalancer/actuator_test.go b/internal/controllers/loadbalancer/actuator_test.go
new file mode 100644
index 000000000..07d70a38f
--- /dev/null
+++ b/internal/controllers/loadbalancer/actuator_test.go
@@ -0,0 +1,185 @@
+/*
+Copyright 2026 The ORC 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 loadbalancer
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "k8s.io/utils/ptr"
+)
+
+func TestNeedsUpdate(t *testing.T) {
+ testCases := []struct {
+ name string
+ updateOpts loadbalancers.UpdateOpts
+ expectChange bool
+ }{
+ {
+ name: "Empty base opts",
+ updateOpts: loadbalancers.UpdateOpts{},
+ expectChange: false,
+ },
+ {
+ name: "Updated opts",
+ updateOpts: loadbalancers.UpdateOpts{Name: ptr.To("updated")},
+ expectChange: true,
+ },
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ got, _ := needsUpdate(tt.updateOpts)
+ if got != tt.expectChange {
+ t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
+ }
+ })
+ }
+}
+
+func TestHandleNameUpdate(t *testing.T) {
+ ptrToName := ptr.To[orcv1alpha1.OpenStackName]
+ testCases := []struct {
+ name string
+ newValue *orcv1alpha1.OpenStackName
+ existingValue string
+ expectChange bool
+ }{
+ {name: "Identical", newValue: ptrToName("name"), existingValue: "name", expectChange: false},
+ {name: "Different", newValue: ptrToName("new-name"), existingValue: "name", expectChange: true},
+ {name: "No value provided, existing is identical to object name", newValue: nil, existingValue: "object-name", expectChange: false},
+ {name: "No value provided, existing is different from object name", newValue: nil, existingValue: "different-from-object-name", expectChange: true},
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ resource := &orcv1alpha1.LoadBalancer{}
+ resource.Name = "object-name"
+ resource.Spec = orcv1alpha1.LoadBalancerSpec{
+ Resource: &orcv1alpha1.LoadBalancerResourceSpec{Name: tt.newValue},
+ }
+ osResource := &osResourceT{Name: tt.existingValue}
+
+ updateOpts := loadbalancers.UpdateOpts{}
+ handleNameUpdate(&updateOpts, resource, osResource)
+
+ got, _ := needsUpdate(updateOpts)
+ if got != tt.expectChange {
+ t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
+ }
+ })
+
+ }
+}
+
+func TestHandleDescriptionUpdate(t *testing.T) {
+ ptrToDescription := ptr.To[string]
+ testCases := []struct {
+ name string
+ newValue *string
+ existingValue string
+ expectChange bool
+ }{
+ {name: "Identical", newValue: ptrToDescription("desc"), existingValue: "desc", expectChange: false},
+ {name: "Different", newValue: ptrToDescription("new-desc"), existingValue: "desc", expectChange: true},
+ {name: "No value provided, existing is set", newValue: nil, existingValue: "desc", expectChange: true},
+ {name: "No value provided, existing is empty", newValue: nil, existingValue: "", expectChange: false},
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ resource := &orcv1alpha1.LoadBalancerResourceSpec{Description: tt.newValue}
+ osResource := &osResourceT{Description: tt.existingValue}
+
+ updateOpts := loadbalancers.UpdateOpts{}
+ handleDescriptionUpdate(&updateOpts, resource, osResource)
+
+ got, _ := needsUpdate(updateOpts)
+ if got != tt.expectChange {
+ t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
+ }
+ })
+
+ }
+}
+
+func TestHandleAdminStateUpdate(t *testing.T) {
+ ptrToBool := ptr.To[bool]
+ testCases := []struct {
+ name string
+ newValue *bool
+ existingValue bool
+ expectChange bool
+ }{
+ {name: "Identical true", newValue: ptrToBool(true), existingValue: true, expectChange: false},
+ {name: "Identical false", newValue: ptrToBool(false), existingValue: false, expectChange: false},
+ {name: "Different true to false", newValue: ptrToBool(false), existingValue: true, expectChange: true},
+ {name: "Different false to true", newValue: ptrToBool(true), existingValue: false, expectChange: true},
+ {name: "No value provided, existing is set to false", newValue: nil, existingValue: false, expectChange: true},
+ {name: "No value provided, existing is default (true)", newValue: nil, existingValue: true, expectChange: false},
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ resource := &orcv1alpha1.LoadBalancerResourceSpec{AdminStateUp: tt.newValue}
+ osResource := &osResourceT{AdminStateUp: tt.existingValue}
+
+ updateOpts := loadbalancers.UpdateOpts{}
+ handleAdminStateUpdate(&updateOpts, resource, osResource)
+
+ got, _ := needsUpdate(updateOpts)
+ if got != tt.expectChange {
+ t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
+ }
+ })
+ }
+}
+
+func TestHandleTagsUpdate(t *testing.T) {
+ testCases := []struct {
+ name string
+ newValue []orcv1alpha1.LoadBalancerTag
+ existingValue []string
+ expectChange bool
+ }{
+ {name: "Identical empty", newValue: nil, existingValue: nil, expectChange: false},
+ {name: "Identical single", newValue: []orcv1alpha1.LoadBalancerTag{"tag1"}, existingValue: []string{"tag1"}, expectChange: false},
+ {name: "Identical multiple", newValue: []orcv1alpha1.LoadBalancerTag{"tag1", "tag2"}, existingValue: []string{"tag1", "tag2"}, expectChange: false},
+ {name: "Identical different order", newValue: []orcv1alpha1.LoadBalancerTag{"tag2", "tag1"}, existingValue: []string{"tag1", "tag2"}, expectChange: false},
+ {name: "Different add tag", newValue: []orcv1alpha1.LoadBalancerTag{"tag1", "tag2"}, existingValue: []string{"tag1"}, expectChange: true},
+ {name: "Different remove tag", newValue: []orcv1alpha1.LoadBalancerTag{"tag1"}, existingValue: []string{"tag1", "tag2"}, expectChange: true},
+ {name: "Different replace tag", newValue: []orcv1alpha1.LoadBalancerTag{"tag1", "tag3"}, existingValue: []string{"tag1", "tag2"}, expectChange: true},
+ {name: "Add tags to empty", newValue: []orcv1alpha1.LoadBalancerTag{"tag1"}, existingValue: nil, expectChange: true},
+ {name: "Remove all tags", newValue: nil, existingValue: []string{"tag1"}, expectChange: true},
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ resource := &orcv1alpha1.LoadBalancerResourceSpec{Tags: tt.newValue}
+ osResource := &osResourceT{Tags: tt.existingValue}
+
+ updateOpts := loadbalancers.UpdateOpts{}
+ handleTagsUpdate(&updateOpts, resource, osResource)
+
+ got, _ := needsUpdate(updateOpts)
+ if got != tt.expectChange {
+ t.Errorf("Expected change: %v, got: %v", tt.expectChange, got)
+ }
+ })
+ }
+}
diff --git a/internal/controllers/loadbalancer/controller.go b/internal/controllers/loadbalancer/controller.go
new file mode 100644
index 000000000..c8dde600d
--- /dev/null
+++ b/internal/controllers/loadbalancer/controller.go
@@ -0,0 +1,261 @@
+/*
+Copyright 2026 The ORC 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 loadbalancer
+
+import (
+ "context"
+ "errors"
+
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/controller"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/reconciler"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/scope"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/credentials"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
+ "github.com/k-orc/openstack-resource-controller/v2/pkg/predicates"
+)
+
+const controllerName = "loadbalancer"
+
+// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=loadbalancers,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=loadbalancers/status,verbs=get;update;patch
+
+type loadbalancerReconcilerConstructor struct {
+ scopeFactory scope.Factory
+}
+
+func New(scopeFactory scope.Factory) interfaces.Controller {
+ return loadbalancerReconcilerConstructor{scopeFactory: scopeFactory}
+}
+
+func (loadbalancerReconcilerConstructor) GetName() string {
+ return controllerName
+}
+
+var subnetDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Subnet](
+ "spec.resource.subnetRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Resource
+ if resource == nil || resource.VipSubnetRef == nil {
+ return nil
+ }
+ return []string{string(*resource.VipSubnetRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var networkDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Network](
+ "spec.resource.networkRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Resource
+ if resource == nil || resource.VipNetworkRef == nil {
+ return nil
+ }
+ return []string{string(*resource.VipNetworkRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var portDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Port](
+ "spec.resource.portRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Resource
+ if resource == nil || resource.VipPortRef == nil {
+ return nil
+ }
+ return []string{string(*resource.VipPortRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var flavorDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Flavor](
+ "spec.resource.flavorRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Resource
+ if resource == nil || resource.FlavorRef == nil {
+ return nil
+ }
+ return []string{string(*resource.FlavorRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var projectDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Project](
+ "spec.resource.projectRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Resource
+ if resource == nil || resource.ProjectRef == nil {
+ return nil
+ }
+ return []string{string(*resource.ProjectRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var vipNetworkImportDependency = dependency.NewDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Network](
+ "spec.import.filter.vipNetworkRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.VipNetworkRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.VipNetworkRef)}
+ },
+)
+
+var projectImportDependency = dependency.NewDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Project](
+ "spec.import.filter.projectRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.ProjectRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.ProjectRef)}
+ },
+)
+
+var vipSubnetImportDependency = dependency.NewDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Subnet](
+ "spec.import.filter.vipSubnetRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.VipSubnetRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.VipSubnetRef)}
+ },
+)
+
+var vipPortImportDependency = dependency.NewDependency[*orcv1alpha1.LoadBalancerList, *orcv1alpha1.Port](
+ "spec.import.filter.vipPortRef",
+ func(loadbalancer *orcv1alpha1.LoadBalancer) []string {
+ resource := loadbalancer.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.VipPortRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.VipPortRef)}
+ },
+)
+
+// SetupWithManager sets up the controller with the Manager.
+func (c loadbalancerReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
+ log := ctrl.LoggerFrom(ctx)
+ k8sClient := mgr.GetClient()
+
+ subnetWatchEventHandler, err := subnetDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ networkWatchEventHandler, err := networkDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ portWatchEventHandler, err := portDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ flavorWatchEventHandler, err := flavorDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ projectWatchEventHandler, err := projectDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ vipNetworkImportWatchEventHandler, err := vipNetworkImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ projectImportWatchEventHandler, err := projectImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ vipSubnetImportWatchEventHandler, err := vipSubnetImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ vipPortImportWatchEventHandler, err := vipPortImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ builder := ctrl.NewControllerManagedBy(mgr).
+ WithOptions(options).
+ Watches(&orcv1alpha1.Subnet{}, subnetWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Subnet{})),
+ ).
+ Watches(&orcv1alpha1.Network{}, networkWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Network{})),
+ ).
+ Watches(&orcv1alpha1.Port{}, portWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Port{})),
+ ).
+ Watches(&orcv1alpha1.Flavor{}, flavorWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Flavor{})),
+ ).
+ Watches(&orcv1alpha1.Project{}, projectWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Project{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Network{}, vipNetworkImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Network{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Project{}, projectImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Project{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Subnet{}, vipSubnetImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Subnet{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Port{}, vipPortImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Port{})),
+ ).
+ For(&orcv1alpha1.LoadBalancer{})
+
+ if err := errors.Join(
+ subnetDependency.AddToManager(ctx, mgr),
+ networkDependency.AddToManager(ctx, mgr),
+ portDependency.AddToManager(ctx, mgr),
+ flavorDependency.AddToManager(ctx, mgr),
+ projectDependency.AddToManager(ctx, mgr),
+ vipNetworkImportDependency.AddToManager(ctx, mgr),
+ projectImportDependency.AddToManager(ctx, mgr),
+ vipSubnetImportDependency.AddToManager(ctx, mgr),
+ vipPortImportDependency.AddToManager(ctx, mgr),
+ credentialsDependency.AddToManager(ctx, mgr),
+ credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency),
+ ); err != nil {
+ return err
+ }
+
+ r := reconciler.NewController(controllerName, mgr.GetClient(), c.scopeFactory, loadbalancerHelperFactory{}, loadbalancerStatusWriter{})
+ return builder.Complete(&r)
+}
diff --git a/internal/controllers/loadbalancer/status.go b/internal/controllers/loadbalancer/status.go
new file mode 100644
index 000000000..df0841135
--- /dev/null
+++ b/internal/controllers/loadbalancer/status.go
@@ -0,0 +1,77 @@
+/*
+Copyright 2026 The ORC 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 loadbalancer
+
+import (
+ "github.com/go-logr/logr"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
+ orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+)
+
+type loadbalancerStatusWriter struct{}
+
+type objectApplyT = orcapplyconfigv1alpha1.LoadBalancerApplyConfiguration
+type statusApplyT = orcapplyconfigv1alpha1.LoadBalancerStatusApplyConfiguration
+
+var _ interfaces.ResourceStatusWriter[*orcv1alpha1.LoadBalancer, *osResourceT, *objectApplyT, *statusApplyT] = loadbalancerStatusWriter{}
+
+func (loadbalancerStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT {
+ return orcapplyconfigv1alpha1.LoadBalancer(name, namespace)
+}
+
+func (loadbalancerStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.LoadBalancer, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) {
+ if osResource == nil {
+ if orcObject.Status.ID == nil {
+ return metav1.ConditionFalse, nil
+ }
+ return metav1.ConditionUnknown, nil
+ }
+
+ switch osResource.ProvisioningStatus {
+ case "ACTIVE":
+ return metav1.ConditionTrue, nil
+ case "ERROR":
+ return metav1.ConditionFalse, nil
+ default:
+ // PENDING_CREATE, PENDING_UPDATE, PENDING_DELETE
+ return metav1.ConditionFalse, progress.WaitingOnOpenStack(progress.WaitingOnReady, loadbalancerActivePollingPeriod)
+ }
+}
+
+func (loadbalancerStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) {
+ resourceStatus := orcapplyconfigv1alpha1.LoadBalancerResourceStatus().
+ WithName(osResource.Name).
+ WithDescription(osResource.Description).
+ WithVipSubnetID(osResource.VipSubnetID).
+ WithVipNetworkID(osResource.VipNetworkID).
+ WithVipPortID(osResource.VipPortID).
+ WithVipAddress(osResource.VipAddress).
+ WithFlavorID(osResource.FlavorID).
+ WithProjectID(osResource.ProjectID).
+ WithAdminStateUp(osResource.AdminStateUp).
+ WithProvider(osResource.Provider).
+ WithAvailabilityZone(osResource.AvailabilityZone).
+ WithProvisioningStatus(osResource.ProvisioningStatus).
+ WithOperatingStatus(osResource.OperatingStatus).
+ WithTags(osResource.Tags...)
+
+ statusApply.WithResource(resourceStatus)
+}
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-assert.yaml
new file mode 100644
index 000000000..95ed0643a
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-assert.yaml
@@ -0,0 +1,53 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-create-full
+status:
+ resource:
+ name: loadbalancer-create-full-override
+ description: LoadBalancer from "create full" test
+ adminStateUp: true
+ provisioningStatus: ACTIVE
+ tags:
+ - tag1
+ - tag2
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-create-full
+ ref: loadbalancer
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Subnet
+ name: loadbalancer-create-full
+ ref: subnet
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Network
+ name: loadbalancer-create-full
+ ref: network
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Port
+ name: loadbalancer-create-full
+ ref: port
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Project
+ name: loadbalancer-create-full
+ ref: project
+assertAll:
+ # Dynamic checks that require CEL - cross-resource comparisons
+ - celExpr: "loadbalancer.status.id != ''"
+ - celExpr: "loadbalancer.status.resource.vipSubnetID == subnet.status.id"
+ - celExpr: "loadbalancer.status.resource.vipNetworkID == network.status.id"
+ - celExpr: "loadbalancer.status.resource.vipPortID == port.status.id"
+ - celExpr: "loadbalancer.status.resource.projectID == project.status.id"
+ - celExpr: "loadbalancer.status.resource.vipAddress != ''"
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-create-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-create-resource.yaml
new file mode 100644
index 000000000..93f84f662
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-create-resource.yaml
@@ -0,0 +1,73 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: loadbalancer-create-full
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-create-full
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-create-full
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-create-full
+ ipVersion: 4
+ cidr: 10.0.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Port
+metadata:
+ name: loadbalancer-create-full
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-create-full
+ addresses:
+ - subnetRef: loadbalancer-create-full
+ ip: 10.0.0.10
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-create-full
+spec:
+ cloudCredentialsRef:
+ # using openstack-admin to create the LB in another project
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: loadbalancer-create-full-override
+ description: LoadBalancer from "create full" test
+ vipSubnetRef: loadbalancer-create-full
+ vipNetworkRef: loadbalancer-create-full
+ vipPortRef: loadbalancer-create-full
+ projectRef: loadbalancer-create-full
+ adminStateUp: true
+ tags:
+ - tag1
+ - tag2
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-full/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/README.md
new file mode 100644
index 000000000..44bd88dfc
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-full/README.md
@@ -0,0 +1,11 @@
+# Create a LoadBalancer with all the options
+
+## Step 00
+
+Create a LoadBalancer using all available fields, and verify that the observed state corresponds to the spec.
+
+Also validate that the OpenStack resource uses the name from the spec when it is specified.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#create-full
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-assert.yaml
new file mode 100644
index 000000000..e59fe853d
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-assert.yaml
@@ -0,0 +1,33 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-create-minimal
+status:
+ resource:
+ name: loadbalancer-create-minimal
+ provisioningStatus: ACTIVE
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-create-minimal
+ ref: loadbalancer
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Subnet
+ name: loadbalancer-create-minimal
+ ref: subnet
+assertAll:
+ # Dynamic checks that require CEL - cross-resource comparisons and non-empty checks
+ - celExpr: "loadbalancer.status.id != ''"
+ - celExpr: "loadbalancer.status.resource.vipSubnetID == subnet.status.id"
+ - celExpr: "loadbalancer.status.resource.vipAddress != ''"
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-create-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-create-resource.yaml
new file mode 100644
index 000000000..d527aa280
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-create-resource.yaml
@@ -0,0 +1,37 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-create-minimal
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-create-minimal
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-create-minimal
+ ipVersion: 4
+ cidr: 10.0.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-create-minimal
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-create-minimal
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/01-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/01-assert.yaml
new file mode 100644
index 000000000..c116ae4b8
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/01-assert.yaml
@@ -0,0 +1,11 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: v1
+ kind: Secret
+ name: openstack-clouds
+ ref: secret
+assertAll:
+ - celExpr: "secret.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in secret.metadata.finalizers"
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/01-delete-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/01-delete-secret.yaml
new file mode 100644
index 000000000..1620791b9
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/01-delete-secret.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ # We expect the deletion to hang due to the finalizer, so use --wait=false
+ - command: kubectl delete secret openstack-clouds --wait=false
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/README.md
new file mode 100644
index 000000000..07e0ca5b6
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-create-minimal/README.md
@@ -0,0 +1,15 @@
+# Create a LoadBalancer with the minimum options
+
+## Step 00
+
+Create a minimal LoadBalancer, that sets only the required fields, and verify that the observed state corresponds to the spec.
+
+Also validate that the OpenStack resource uses the name of the ORC object when no name is explicitly specified.
+
+## Step 01
+
+Try deleting the secret and ensure that it is not deleted thanks to the finalizer.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#create-minimal
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-assert.yaml
new file mode 100644
index 000000000..866470662
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-assert.yaml
@@ -0,0 +1,102 @@
+---
+# Wait for infrastructure to be ready first
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-dependency-infra
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-dependency-subnet
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-secret
+status:
+ conditions:
+ - type: Available
+ message: Waiting for Secret/loadbalancer-dependency to be created
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for Secret/loadbalancer-dependency to be created
+ status: "True"
+ reason: Progressing
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-subnet
+status:
+ conditions:
+ - type: Available
+ message: Waiting for Subnet/loadbalancer-dependency to be created
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for Subnet/loadbalancer-dependency to be created
+ status: "True"
+ reason: Progressing
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-network
+status:
+ conditions:
+ - type: Available
+ message: Waiting for Network/loadbalancer-dependency to be created
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for Network/loadbalancer-dependency to be created
+ status: "True"
+ reason: Progressing
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-port
+status:
+ conditions:
+ - type: Available
+ message: Waiting for Port/loadbalancer-dependency to be created
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for Port/loadbalancer-dependency to be created
+ status: "True"
+ reason: Progressing
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-project
+status:
+ conditions:
+ - type: Available
+ message: Waiting for Project/loadbalancer-dependency to be created
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for Project/loadbalancer-dependency to be created
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-create-resources-missing-deps.yaml
new file mode 100644
index 000000000..14046fb6b
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-create-resources-missing-deps.yaml
@@ -0,0 +1,88 @@
+---
+# Create network/subnet infrastructure for tests that need existing subnet
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-dependency-infra
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-dependency-subnet
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-dependency-infra
+ ipVersion: 4
+ cidr: 10.0.99.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-subnet
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-network
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipNetworkRef: loadbalancer-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-port
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipPortRef: loadbalancer-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-project
+spec:
+ cloudCredentialsRef:
+ # using openstack-admin to create the LB in another project
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-dependency-subnet
+ projectRef: loadbalancer-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-secret
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: loadbalancer-dependency
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-dependency-subnet
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/01-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/01-assert.yaml
new file mode 100644
index 000000000..ec74c08e9
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/01-assert.yaml
@@ -0,0 +1,65 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-secret
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-subnet
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-network
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-port
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-dependency-no-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/01-create-dependencies.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/01-create-dependencies.yaml
new file mode 100644
index 000000000..9b2f23873
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/01-create-dependencies.yaml
@@ -0,0 +1,57 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic loadbalancer-dependency --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: loadbalancer-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-dependency
+ ipVersion: 4
+ cidr: 10.0.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Port
+metadata:
+ name: loadbalancer-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-dependency
+ addresses:
+ - subnetRef: loadbalancer-dependency
+ ip: 10.0.0.10
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/02-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/02-assert.yaml
new file mode 100644
index 000000000..850000719
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/02-assert.yaml
@@ -0,0 +1,41 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Subnet
+ name: loadbalancer-dependency
+ ref: subnet
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Subnet
+ name: loadbalancer-dependency-subnet
+ ref: subnet2
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Network
+ name: loadbalancer-dependency
+ ref: network
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Port
+ name: loadbalancer-dependency
+ ref: port
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Project
+ name: loadbalancer-dependency
+ ref: project
+ - apiVersion: v1
+ kind: Secret
+ name: loadbalancer-dependency
+ ref: secret
+assertAll:
+ - celExpr: "subnet.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in subnet.metadata.finalizers"
+ - celExpr: "subnet2.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in subnet2.metadata.finalizers"
+ - celExpr: "network.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in network.metadata.finalizers"
+ - celExpr: "port.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in port.metadata.finalizers"
+ - celExpr: "project.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in project.metadata.finalizers"
+ - celExpr: "secret.metadata.deletionTimestamp != 0"
+ - celExpr: "'openstack.k-orc.cloud/loadbalancer' in secret.metadata.finalizers"
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/02-delete-dependencies.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/02-delete-dependencies.yaml
new file mode 100644
index 000000000..869b3d361
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/02-delete-dependencies.yaml
@@ -0,0 +1,19 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ # We expect the deletion to hang due to the finalizer, so use --wait=false
+ - command: kubectl delete subnet loadbalancer-dependency --wait=false
+ namespaced: true
+ - command: kubectl delete subnet loadbalancer-dependency-subnet --wait=false
+ namespaced: true
+ - command: kubectl delete network loadbalancer-dependency --wait=false
+ namespaced: true
+ - command: kubectl delete network loadbalancer-dependency-infra --wait=false
+ namespaced: true
+ - command: kubectl delete port loadbalancer-dependency --wait=false
+ namespaced: true
+ - command: kubectl delete project loadbalancer-dependency --wait=false
+ namespaced: true
+ - command: kubectl delete secret loadbalancer-dependency --wait=false
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/03-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/03-assert.yaml
new file mode 100644
index 000000000..26f0781b8
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/03-assert.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+# Dependencies that were prevented deletion before should now be gone
+- script: "! kubectl get subnet loadbalancer-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get subnet loadbalancer-dependency-subnet --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get network loadbalancer-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get port loadbalancer-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get project loadbalancer-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get secret loadbalancer-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/03-delete-resources.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/03-delete-resources.yaml
new file mode 100644
index 000000000..b4eb49b0c
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/03-delete-resources.yaml
@@ -0,0 +1,19 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+- apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-dependency-no-secret
+- apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-dependency-no-subnet
+- apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-dependency-no-network
+- apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-dependency-no-port
+- apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-dependency-no-project
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-dependency/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/README.md
new file mode 100644
index 000000000..b1a05802f
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-dependency/README.md
@@ -0,0 +1,21 @@
+# Creation and deletion dependencies
+
+## Step 00
+
+Create LoadBalancers referencing non-existing resources. Each LoadBalancer is dependent on other non-existing resource. Verify that the LoadBalancers are waiting for the needed resources to be created externally.
+
+## Step 01
+
+Create the missing dependencies and verify all the LoadBalancers are available.
+
+## Step 02
+
+Delete all the dependencies and check that ORC prevents deletion since there is still a resource that depends on them.
+
+## Step 03
+
+Delete the LoadBalancers and validate that all resources are gone.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#dependency
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-assert.yaml
new file mode 100644
index 000000000..5f28dade2
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-assert.yaml
@@ -0,0 +1,23 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency
+status:
+ conditions:
+ - type: Available
+ message: |-
+ Waiting for Network/loadbalancer-import-dependency to be ready
+ Waiting for Project/loadbalancer-import-dependency to be ready
+ Waiting for Subnet/loadbalancer-import-dependency to be ready
+ Waiting for Port/loadbalancer-import-dependency to be ready
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: |-
+ Waiting for Network/loadbalancer-import-dependency to be ready
+ Waiting for Project/loadbalancer-import-dependency to be ready
+ Waiting for Subnet/loadbalancer-import-dependency to be ready
+ Waiting for Port/loadbalancer-import-dependency to be ready
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-import-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-import-resource.yaml
new file mode 100644
index 000000000..c236d41bf
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-import-resource.yaml
@@ -0,0 +1,71 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-import-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ name: loadbalancer-import-dependency-external
+ projectRef: loadbalancer-import-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: loadbalancer-import-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ name: loadbalancer-import-dependency-external
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-import-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ name: loadbalancer-import-dependency-external
+ projectRef: loadbalancer-import-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Port
+metadata:
+ name: loadbalancer-import-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ name: loadbalancer-import-dependency-external
+ projectRef: loadbalancer-import-dependency
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ vipNetworkRef: loadbalancer-import-dependency
+ projectRef: loadbalancer-import-dependency
+ vipSubnetRef: loadbalancer-import-dependency
+ vipPortRef: loadbalancer-import-dependency
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/01-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/01-assert.yaml
new file mode 100644
index 000000000..18e4398f5
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/01-assert.yaml
@@ -0,0 +1,38 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency-not-this-one
+status:
+ conditions:
+ - type: Available
+ message: OpenStack resource is available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ message: OpenStack resource is up to date
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency
+status:
+ conditions:
+ - type: Available
+ message: |-
+ Waiting for Network/loadbalancer-import-dependency to be ready
+ Waiting for Project/loadbalancer-import-dependency to be ready
+ Waiting for Subnet/loadbalancer-import-dependency to be ready
+ Waiting for Port/loadbalancer-import-dependency to be ready
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: |-
+ Waiting for Network/loadbalancer-import-dependency to be ready
+ Waiting for Project/loadbalancer-import-dependency to be ready
+ Waiting for Subnet/loadbalancer-import-dependency to be ready
+ Waiting for Port/loadbalancer-import-dependency to be ready
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/01-create-trap-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/01-create-trap-resource.yaml
new file mode 100644
index 000000000..9a89f4512
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/01-create-trap-resource.yaml
@@ -0,0 +1,69 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: loadbalancer-import-dependency-not-this-one
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-import-dependency-not-this-one
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ projectRef: loadbalancer-import-dependency-not-this-one
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-import-dependency-not-this-one
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-import-dependency-not-this-one
+ projectRef: loadbalancer-import-dependency-not-this-one
+ ipVersion: 4
+ cidr: 10.200.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Port
+metadata:
+ name: loadbalancer-import-dependency-not-this-one
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-import-dependency-not-this-one
+ projectRef: loadbalancer-import-dependency-not-this-one
+ addresses:
+ - subnetRef: loadbalancer-import-dependency-not-this-one
+ ip: 10.200.0.10
+---
+# This `loadbalancer-import-dependency-not-this-one` should not be picked by the import filter
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency-not-this-one
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipPortRef: loadbalancer-import-dependency-not-this-one
+ vipSubnetRef: loadbalancer-import-dependency-not-this-one
+ projectRef: loadbalancer-import-dependency-not-this-one
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/02-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/02-assert.yaml
new file mode 100644
index 000000000..0e3edc29a
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/02-assert.yaml
@@ -0,0 +1,49 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-import-dependency
+ ref: loadbalancer1
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-import-dependency-not-this-one
+ ref: loadbalancer2
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Network
+ name: loadbalancer-import-dependency
+ ref: network
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Project
+ name: loadbalancer-import-dependency
+ ref: project
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Subnet
+ name: loadbalancer-import-dependency
+ ref: subnet
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Port
+ name: loadbalancer-import-dependency
+ ref: port
+assertAll:
+ - celExpr: "loadbalancer1.status.id != loadbalancer2.status.id"
+ - celExpr: "loadbalancer1.status.resource.vipNetworkID == network.status.id"
+ - celExpr: "loadbalancer1.status.resource.projectID == project.status.id"
+ - celExpr: "loadbalancer1.status.resource.vipSubnetID == subnet.status.id"
+ - celExpr: "loadbalancer1.status.resource.vipPortID == port.status.id"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency
+status:
+ conditions:
+ - type: Available
+ message: OpenStack resource is available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ message: OpenStack resource is up to date
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/02-create-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/02-create-resource.yaml
new file mode 100644
index 000000000..6044ebecd
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/02-create-resource.yaml
@@ -0,0 +1,68 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: loadbalancer-import-dependency-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-import-dependency-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ projectRef: loadbalancer-import-dependency-external
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-import-dependency-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-import-dependency-external
+ projectRef: loadbalancer-import-dependency-external
+ ipVersion: 4
+ cidr: 10.201.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Port
+metadata:
+ name: loadbalancer-import-dependency-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-import-dependency-external
+ projectRef: loadbalancer-import-dependency-external
+ addresses:
+ - subnetRef: loadbalancer-import-dependency-external
+ ip: 10.201.0.10
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-dependency-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipPortRef: loadbalancer-import-dependency-external
+ vipSubnetRef: loadbalancer-import-dependency-external
+ projectRef: loadbalancer-import-dependency-external
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/03-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/03-assert.yaml
new file mode 100644
index 000000000..0ff111fc5
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/03-assert.yaml
@@ -0,0 +1,12 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+- script: "! kubectl get network loadbalancer-import-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get project loadbalancer-import-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get subnet loadbalancer-import-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+- script: "! kubectl get port loadbalancer-import-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/03-delete-import-dependencies.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/03-delete-import-dependencies.yaml
new file mode 100644
index 000000000..e6be2bd79
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/03-delete-import-dependencies.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ # We should be able to delete the import dependencies
+ - command: kubectl delete network loadbalancer-import-dependency
+ namespaced: true
+ - command: kubectl delete project loadbalancer-import-dependency
+ namespaced: true
+ - command: kubectl delete subnet loadbalancer-import-dependency
+ namespaced: true
+ - command: kubectl delete port loadbalancer-import-dependency
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/04-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/04-assert.yaml
new file mode 100644
index 000000000..0aa186534
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/04-assert.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+- script: "! kubectl get loadbalancer loadbalancer-import-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/04-delete-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/04-delete-resource.yaml
new file mode 100644
index 000000000..4d83e9aff
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/04-delete-resource.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-import-dependency
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/README.md
new file mode 100644
index 000000000..15004d8ca
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-dependency/README.md
@@ -0,0 +1,29 @@
+# Check dependency handling for imported LoadBalancer
+
+## Step 00
+
+Import a LoadBalancer that references other imported resources. The referenced imported resources have no matching resources yet.
+Verify the LoadBalancer is waiting for the dependency to be ready.
+
+## Step 01
+
+Create a LoadBalancer matching the import filter, except for referenced resources, and verify that it's not being imported.
+
+## Step 02
+
+Create the referenced resources and a LoadBalancer matching the import filters.
+
+Verify that the observed status on the imported LoadBalancer corresponds to the spec of the created LoadBalancer.
+
+## Step 03
+
+Delete the referenced resources and check that ORC does not prevent deletion. The OpenStack resources still exist because they
+were imported resources and we only deleted the ORC representation of it.
+
+## Step 04
+
+Delete the LoadBalancer and validate that all resources are gone.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#import-dependency
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-assert.yaml
new file mode 100644
index 000000000..3f41cc651
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-assert.yaml
@@ -0,0 +1,26 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-error-external-1
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-error-external-2
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-create-resources.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-create-resources.yaml
new file mode 100644
index 000000000..94cf2dd6b
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-create-resources.yaml
@@ -0,0 +1,51 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-import-error
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-import-error
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-import-error
+ ipVersion: 4
+ cidr: 10.0.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-error-external-1
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-import-error
+ description: LoadBalancer from "import error" test
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-error-external-2
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-import-error
+ description: LoadBalancer from "import error" test
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-error/01-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/01-assert.yaml
new file mode 100644
index 000000000..1629ec940
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/01-assert.yaml
@@ -0,0 +1,15 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-error
+status:
+ conditions:
+ - type: Available
+ message: found more than one matching OpenStack resource during import
+ status: "False"
+ reason: InvalidConfiguration
+ - type: Progressing
+ message: found more than one matching OpenStack resource during import
+ status: "False"
+ reason: InvalidConfiguration
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-error/01-import-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/01-import-resource.yaml
new file mode 100644
index 000000000..23918c0b0
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/01-import-resource.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-error
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ description: LoadBalancer from "import error" test
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import-error/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/README.md
new file mode 100644
index 000000000..3f9d4cf84
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import-error/README.md
@@ -0,0 +1,13 @@
+# Import LoadBalancer with more than one matching resources
+
+## Step 00
+
+Create two LoadBalancers with identical specs.
+
+## Step 01
+
+Ensure that an imported LoadBalancer with a filter matching the resources returns an error.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#import-error
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/00-assert.yaml
new file mode 100644
index 000000000..86f108f68
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/00-assert.yaml
@@ -0,0 +1,15 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import
+status:
+ conditions:
+ - type: Available
+ message: Waiting for OpenStack resource to be created externally
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for OpenStack resource to be created externally
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/00-import-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/00-import-resource.yaml
new file mode 100644
index 000000000..93c311aeb
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/00-import-resource.yaml
@@ -0,0 +1,39 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-import
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-import
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-import
+ ipVersion: 4
+ cidr: 10.0.0.0/24
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ name: loadbalancer-import-external
+ description: LoadBalancer loadbalancer-import-external from "loadbalancer-import" test
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/00-secret.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/01-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/01-assert.yaml
new file mode 100644
index 000000000..d7d034bd0
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/01-assert.yaml
@@ -0,0 +1,31 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-external-not-this-one
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+ resource:
+ name: loadbalancer-import-external-not-this-one
+ description: LoadBalancer loadbalancer-import-external from "loadbalancer-import" test
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import
+status:
+ conditions:
+ - type: Available
+ message: Waiting for OpenStack resource to be created externally
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for OpenStack resource to be created externally
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/01-create-trap-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/01-create-trap-resource.yaml
new file mode 100644
index 000000000..6b4f82a35
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/01-create-trap-resource.yaml
@@ -0,0 +1,16 @@
+---
+# This `loadbalancer-import-external-not-this-one` resource serves two purposes:
+# - ensure that we can successfully create another resource which name is a substring of it (i.e. it's not being adopted)
+# - ensure that importing a resource which name is a substring of it will not pick this one.
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-external-not-this-one
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-import
+ description: LoadBalancer loadbalancer-import-external from "loadbalancer-import" test
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/02-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/02-assert.yaml
new file mode 100644
index 000000000..17e021a11
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/02-assert.yaml
@@ -0,0 +1,35 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-import-external
+ ref: loadbalancer1
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-import-external-not-this-one
+ ref: loadbalancer2
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-import
+ ref: loadbalancerImport
+assertAll:
+ - celExpr: "loadbalancer1.status.id != loadbalancer2.status.id"
+ - celExpr: "loadbalancerImport.status.id == loadbalancer1.status.id"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+ resource:
+ name: loadbalancer-import-external
+ description: LoadBalancer loadbalancer-import-external from "loadbalancer-import" test
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/02-create-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-import/02-create-resource.yaml
new file mode 100644
index 000000000..a0ca7b241
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/02-create-resource.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-import-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-import
+ description: LoadBalancer loadbalancer-import-external from "loadbalancer-import" test
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-import/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-import/README.md
new file mode 100644
index 000000000..090224429
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-import/README.md
@@ -0,0 +1,18 @@
+# Import LoadBalancer
+
+## Step 00
+
+Import a loadbalancer that matches all fields in the filter, and verify it is waiting for the external resource to be created.
+
+## Step 01
+
+Create a loadbalancer whose name is a superstring of the one specified in the import filter, otherwise matching the filter, and verify that it's not being imported.
+
+## Step 02
+
+Create a loadbalancer matching the filter and verify that the observed status on the imported loadbalancer corresponds to the spec of the created loadbalancer.
+Also, confirm that it does not adopt any loadbalancer whose name is a superstring of its own.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#import
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/00-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/00-assert.yaml
new file mode 100644
index 000000000..554c1157d
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/00-assert.yaml
@@ -0,0 +1,25 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-update
+ ref: loadbalancer
+assertAll:
+ - celExpr: "loadbalancer.status.resource.description == ''"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-update
+status:
+ resource:
+ name: loadbalancer-update
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/00-minimal-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/00-minimal-resource.yaml
new file mode 100644
index 000000000..cfa04301b
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/00-minimal-resource.yaml
@@ -0,0 +1,12 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-update
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ vipSubnetRef: loadbalancer-update
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/00-prerequisites.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/00-prerequisites.yaml
new file mode 100644
index 000000000..5d74c652d
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/00-prerequisites.yaml
@@ -0,0 +1,31 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT}
+ namespaced: true
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Network
+metadata:
+ name: loadbalancer-update
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Subnet
+metadata:
+ name: loadbalancer-update
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ networkRef: loadbalancer-update
+ ipVersion: 4
+ cidr: 10.0.0.0/24
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/01-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/01-assert.yaml
new file mode 100644
index 000000000..bdfe8f077
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/01-assert.yaml
@@ -0,0 +1,19 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-update
+status:
+ resource:
+ name: loadbalancer-update-updated
+ description: loadbalancer-update-updated
+ adminStateUp: false
+ tags:
+ - updated-tag
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/01-updated-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/01-updated-resource.yaml
new file mode 100644
index 000000000..b8fc612de
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/01-updated-resource.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-update
+spec:
+ resource:
+ vipSubnetRef: loadbalancer-update
+ name: loadbalancer-update-updated
+ description: loadbalancer-update-updated
+ adminStateUp: false
+ tags:
+ - updated-tag
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/02-assert.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/02-assert.yaml
new file mode 100644
index 000000000..bd430a3dd
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/02-assert.yaml
@@ -0,0 +1,26 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: LoadBalancer
+ name: loadbalancer-update
+ ref: loadbalancer
+assertAll:
+ - celExpr: "loadbalancer.status.resource.description == ''"
+ - celExpr: "!has(loadbalancer.status.resource.tags)"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: LoadBalancer
+metadata:
+ name: loadbalancer-update
+status:
+ resource:
+ name: loadbalancer-update
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/02-reverted-resource.yaml b/internal/controllers/loadbalancer/tests/loadbalancer-update/02-reverted-resource.yaml
new file mode 100644
index 000000000..2c6c253ff
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/02-reverted-resource.yaml
@@ -0,0 +1,7 @@
+# NOTE: kuttl only does patch updates, which means we can't delete a field.
+# We have to use a kubectl apply command instead.
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl replace -f 00-minimal-resource.yaml
+ namespaced: true
diff --git a/internal/controllers/loadbalancer/tests/loadbalancer-update/README.md b/internal/controllers/loadbalancer/tests/loadbalancer-update/README.md
new file mode 100644
index 000000000..9dafb19c4
--- /dev/null
+++ b/internal/controllers/loadbalancer/tests/loadbalancer-update/README.md
@@ -0,0 +1,17 @@
+# Update LoadBalancer
+
+## Step 00
+
+Create a LoadBalancer using only mandatory fields.
+
+## Step 01
+
+Update all mutable fields.
+
+## Step 02
+
+Revert the resource to its original value and verify that the resulting object matches its state when first created.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#update
diff --git a/internal/controllers/loadbalancer/zz_generated.adapter.go b/internal/controllers/loadbalancer/zz_generated.adapter.go
new file mode 100644
index 000000000..f4f517939
--- /dev/null
+++ b/internal/controllers/loadbalancer/zz_generated.adapter.go
@@ -0,0 +1,88 @@
+// Code generated by resource-generator. DO NOT EDIT.
+/*
+Copyright The ORC 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 loadbalancer
+
+import (
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+)
+
+// Fundamental types
+type (
+ orcObjectT = orcv1alpha1.LoadBalancer
+ orcObjectListT = orcv1alpha1.LoadBalancerList
+ resourceSpecT = orcv1alpha1.LoadBalancerResourceSpec
+ filterT = orcv1alpha1.LoadBalancerFilter
+)
+
+// Derived types
+type (
+ orcObjectPT = *orcObjectT
+ adapterI = interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT]
+ adapterT = loadbalancerAdapter
+)
+
+type loadbalancerAdapter struct {
+ *orcv1alpha1.LoadBalancer
+}
+
+var _ adapterI = &adapterT{}
+
+func (f adapterT) GetObject() orcObjectPT {
+ return f.LoadBalancer
+}
+
+func (f adapterT) GetManagementPolicy() orcv1alpha1.ManagementPolicy {
+ return f.Spec.ManagementPolicy
+}
+
+func (f adapterT) GetManagedOptions() *orcv1alpha1.ManagedOptions {
+ return f.Spec.ManagedOptions
+}
+
+func (f adapterT) GetStatusID() *string {
+ return f.Status.ID
+}
+
+func (f adapterT) GetResourceSpec() *resourceSpecT {
+ return f.Spec.Resource
+}
+
+func (f adapterT) GetImportID() *string {
+ if f.Spec.Import == nil {
+ return nil
+ }
+ return f.Spec.Import.ID
+}
+
+func (f adapterT) GetImportFilter() *filterT {
+ if f.Spec.Import == nil {
+ return nil
+ }
+ return f.Spec.Import.Filter
+}
+
+// getResourceName returns the name of the OpenStack resource we should use.
+// This method is not implemented as part of APIObjectAdapter as it is intended
+// to be used by resource actuators, which don't use the adapter.
+func getResourceName(orcObject orcObjectPT) string {
+ if orcObject.Spec.Resource.Name != nil {
+ return string(*orcObject.Spec.Resource.Name)
+ }
+ return orcObject.Name
+}
diff --git a/internal/controllers/loadbalancer/zz_generated.controller.go b/internal/controllers/loadbalancer/zz_generated.controller.go
new file mode 100644
index 000000000..cc3bdf8df
--- /dev/null
+++ b/internal/controllers/loadbalancer/zz_generated.controller.go
@@ -0,0 +1,45 @@
+// Code generated by resource-generator. DO NOT EDIT.
+/*
+Copyright The ORC 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 loadbalancer
+
+import (
+ corev1 "k8s.io/api/core/v1"
+
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
+ orcstrings "github.com/k-orc/openstack-resource-controller/v2/internal/util/strings"
+)
+
+var (
+ // NOTE: controllerName must be defined in any controller using this template
+
+ // finalizer is the string this controller adds to an object's Finalizers
+ finalizer = orcstrings.GetFinalizerName(controllerName)
+
+ // externalObjectFieldOwner is the field owner we use when using
+ // server-side-apply on objects we don't control
+ externalObjectFieldOwner = orcstrings.GetSSAFieldOwner(controllerName)
+
+ credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret](
+ "spec.cloudCredentialsRef.secretName",
+ func(obj orcObjectPT) []string {
+ return []string{obj.Spec.CloudCredentialsRef.SecretName}
+ },
+ finalizer, externalObjectFieldOwner,
+ dependency.OverrideDependencyName("credentials"),
+ )
+)
diff --git a/internal/osclients/loadbalancer.go b/internal/osclients/loadbalancer.go
new file mode 100644
index 000000000..e7ec912ce
--- /dev/null
+++ b/internal/osclients/loadbalancer.go
@@ -0,0 +1,104 @@
+/*
+Copyright 2026 The ORC 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 osclients
+
+import (
+ "context"
+ "fmt"
+ "iter"
+
+ "github.com/gophercloud/gophercloud/v2"
+ "github.com/gophercloud/gophercloud/v2/openstack"
+ "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
+ "github.com/gophercloud/utils/v2/openstack/clientconfig"
+)
+
+type LoadBalancerClient interface {
+ ListLoadBalancers(ctx context.Context, listOpts loadbalancers.ListOptsBuilder) iter.Seq2[*loadbalancers.LoadBalancer, error]
+ CreateLoadBalancer(ctx context.Context, opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error)
+ DeleteLoadBalancer(ctx context.Context, resourceID string) error
+ GetLoadBalancer(ctx context.Context, resourceID string) (*loadbalancers.LoadBalancer, error)
+ UpdateLoadBalancer(ctx context.Context, id string, opts loadbalancers.UpdateOptsBuilder) (*loadbalancers.LoadBalancer, error)
+}
+
+type loadbalancerClient struct{ client *gophercloud.ServiceClient }
+
+// NewLoadBalancerClient returns a new OpenStack client.
+func NewLoadBalancerClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (LoadBalancerClient, error) {
+ client, err := openstack.NewLoadBalancerV2(providerClient, gophercloud.EndpointOpts{
+ Region: providerClientOpts.RegionName,
+ Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType),
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to create loadbalancer service client: %v", err)
+ }
+
+ return &loadbalancerClient{client}, nil
+}
+
+func (c loadbalancerClient) ListLoadBalancers(ctx context.Context, listOpts loadbalancers.ListOptsBuilder) iter.Seq2[*loadbalancers.LoadBalancer, error] {
+ pager := loadbalancers.List(c.client, listOpts)
+ return func(yield func(*loadbalancers.LoadBalancer, error) bool) {
+ _ = pager.EachPage(ctx, yieldPage(loadbalancers.ExtractLoadBalancers, yield))
+ }
+}
+
+func (c loadbalancerClient) CreateLoadBalancer(ctx context.Context, opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) {
+ return loadbalancers.Create(ctx, c.client, opts).Extract()
+}
+
+func (c loadbalancerClient) DeleteLoadBalancer(ctx context.Context, resourceID string) error {
+ return loadbalancers.Delete(ctx, c.client, resourceID, nil).ExtractErr()
+}
+
+func (c loadbalancerClient) GetLoadBalancer(ctx context.Context, resourceID string) (*loadbalancers.LoadBalancer, error) {
+ return loadbalancers.Get(ctx, c.client, resourceID).Extract()
+}
+
+func (c loadbalancerClient) UpdateLoadBalancer(ctx context.Context, id string, opts loadbalancers.UpdateOptsBuilder) (*loadbalancers.LoadBalancer, error) {
+ return loadbalancers.Update(ctx, c.client, id, opts).Extract()
+}
+
+type loadbalancerErrorClient struct{ error }
+
+// NewLoadBalancerErrorClient returns a LoadBalancerClient in which every method returns the given error.
+func NewLoadBalancerErrorClient(e error) LoadBalancerClient {
+ return loadbalancerErrorClient{e}
+}
+
+func (e loadbalancerErrorClient) ListLoadBalancers(_ context.Context, _ loadbalancers.ListOptsBuilder) iter.Seq2[*loadbalancers.LoadBalancer, error] {
+ return func(yield func(*loadbalancers.LoadBalancer, error) bool) {
+ yield(nil, e.error)
+ }
+}
+
+func (e loadbalancerErrorClient) CreateLoadBalancer(_ context.Context, _ loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) {
+ return nil, e.error
+}
+
+func (e loadbalancerErrorClient) DeleteLoadBalancer(_ context.Context, _ string) error {
+ return e.error
+}
+
+func (e loadbalancerErrorClient) GetLoadBalancer(_ context.Context, _ string) (*loadbalancers.LoadBalancer, error) {
+ return nil, e.error
+}
+
+func (e loadbalancerErrorClient) UpdateLoadBalancer(_ context.Context, _ string, _ loadbalancers.UpdateOptsBuilder) (*loadbalancers.LoadBalancer, error) {
+ return nil, e.error
+}
diff --git a/internal/osclients/mock/doc.go b/internal/osclients/mock/doc.go
index 0da3b97ba..d3d3c73b7 100644
--- a/internal/osclients/mock/doc.go
+++ b/internal/osclients/mock/doc.go
@@ -44,6 +44,9 @@ import (
//go:generate mockgen -package mock -destination=keypair.go -source=../keypair.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock KeyPairClient
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt keypair.go > _keypair.go && mv _keypair.go keypair.go"
+//go:generate mockgen -package mock -destination=loadbalancer.go -source=../loadbalancer.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock LoadBalancerClient
+//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt loadbalancer.go > _loadbalancer.go && mv _loadbalancer.go loadbalancer.go"
+
//go:generate mockgen -package mock -destination=role.go -source=../role.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock RoleClient
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt role.go > _role.go && mv _role.go role.go"
diff --git a/internal/osclients/mock/loadbalancer.go b/internal/osclients/mock/loadbalancer.go
new file mode 100644
index 000000000..aae159cd9
--- /dev/null
+++ b/internal/osclients/mock/loadbalancer.go
@@ -0,0 +1,131 @@
+/*
+Copyright The ORC 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 MockGen. DO NOT EDIT.
+// Source: ../loadbalancer.go
+//
+// Generated by this command:
+//
+// mockgen -package mock -destination=loadbalancer.go -source=../loadbalancer.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock LoadBalancerClient
+//
+
+// Package mock is a generated GoMock package.
+package mock
+
+import (
+ context "context"
+ iter "iter"
+ reflect "reflect"
+
+ loadbalancers "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockLoadBalancerClient is a mock of LoadBalancerClient interface.
+type MockLoadBalancerClient struct {
+ ctrl *gomock.Controller
+ recorder *MockLoadBalancerClientMockRecorder
+ isgomock struct{}
+}
+
+// MockLoadBalancerClientMockRecorder is the mock recorder for MockLoadBalancerClient.
+type MockLoadBalancerClientMockRecorder struct {
+ mock *MockLoadBalancerClient
+}
+
+// NewMockLoadBalancerClient creates a new mock instance.
+func NewMockLoadBalancerClient(ctrl *gomock.Controller) *MockLoadBalancerClient {
+ mock := &MockLoadBalancerClient{ctrl: ctrl}
+ mock.recorder = &MockLoadBalancerClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLoadBalancerClient) EXPECT() *MockLoadBalancerClientMockRecorder {
+ return m.recorder
+}
+
+// CreateLoadBalancer mocks base method.
+func (m *MockLoadBalancerClient) CreateLoadBalancer(ctx context.Context, opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "CreateLoadBalancer", ctx, opts)
+ ret0, _ := ret[0].(*loadbalancers.LoadBalancer)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// CreateLoadBalancer indicates an expected call of CreateLoadBalancer.
+func (mr *MockLoadBalancerClientMockRecorder) CreateLoadBalancer(ctx, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLoadBalancer", reflect.TypeOf((*MockLoadBalancerClient)(nil).CreateLoadBalancer), ctx, opts)
+}
+
+// DeleteLoadBalancer mocks base method.
+func (m *MockLoadBalancerClient) DeleteLoadBalancer(ctx context.Context, resourceID string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DeleteLoadBalancer", ctx, resourceID)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// DeleteLoadBalancer indicates an expected call of DeleteLoadBalancer.
+func (mr *MockLoadBalancerClientMockRecorder) DeleteLoadBalancer(ctx, resourceID any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLoadBalancer", reflect.TypeOf((*MockLoadBalancerClient)(nil).DeleteLoadBalancer), ctx, resourceID)
+}
+
+// GetLoadBalancer mocks base method.
+func (m *MockLoadBalancerClient) GetLoadBalancer(ctx context.Context, resourceID string) (*loadbalancers.LoadBalancer, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetLoadBalancer", ctx, resourceID)
+ ret0, _ := ret[0].(*loadbalancers.LoadBalancer)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetLoadBalancer indicates an expected call of GetLoadBalancer.
+func (mr *MockLoadBalancerClientMockRecorder) GetLoadBalancer(ctx, resourceID any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancer", reflect.TypeOf((*MockLoadBalancerClient)(nil).GetLoadBalancer), ctx, resourceID)
+}
+
+// ListLoadBalancers mocks base method.
+func (m *MockLoadBalancerClient) ListLoadBalancers(ctx context.Context, listOpts loadbalancers.ListOptsBuilder) iter.Seq2[*loadbalancers.LoadBalancer, error] {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListLoadBalancers", ctx, listOpts)
+ ret0, _ := ret[0].(iter.Seq2[*loadbalancers.LoadBalancer, error])
+ return ret0
+}
+
+// ListLoadBalancers indicates an expected call of ListLoadBalancers.
+func (mr *MockLoadBalancerClientMockRecorder) ListLoadBalancers(ctx, listOpts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLoadBalancers", reflect.TypeOf((*MockLoadBalancerClient)(nil).ListLoadBalancers), ctx, listOpts)
+}
+
+// UpdateLoadBalancer mocks base method.
+func (m *MockLoadBalancerClient) UpdateLoadBalancer(ctx context.Context, id string, opts loadbalancers.UpdateOptsBuilder) (*loadbalancers.LoadBalancer, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UpdateLoadBalancer", ctx, id, opts)
+ ret0, _ := ret[0].(*loadbalancers.LoadBalancer)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// UpdateLoadBalancer indicates an expected call of UpdateLoadBalancer.
+func (mr *MockLoadBalancerClientMockRecorder) UpdateLoadBalancer(ctx, id, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLoadBalancer", reflect.TypeOf((*MockLoadBalancerClient)(nil).UpdateLoadBalancer), ctx, id, opts)
+}
diff --git a/internal/scope/mock.go b/internal/scope/mock.go
index ef959fae5..61c460564 100644
--- a/internal/scope/mock.go
+++ b/internal/scope/mock.go
@@ -34,17 +34,18 @@ import (
// MockScopeFactory implements both the ScopeFactory and ClientScope interfaces. It can be used in place of the default ProviderScopeFactory
// when we want to use mocked service clients which do not attempt to connect to a running OpenStack cloud.
type MockScopeFactory struct {
- ComputeClient *mock.MockComputeClient
- DomainClient *mock.MockDomainClient
- GroupClient *mock.MockGroupClient
- IdentityClient *mock.MockIdentityClient
- ImageClient *mock.MockImageClient
- KeyPairClient *mock.MockKeyPairClient
- NetworkClient *mock.MockNetworkClient
- RoleClient *mock.MockRoleClient
- ServiceClient *mock.MockServiceClient
- VolumeClient *mock.MockVolumeClient
- VolumeTypeClient *mock.MockVolumeTypeClient
+ ComputeClient *mock.MockComputeClient
+ DomainClient *mock.MockDomainClient
+ GroupClient *mock.MockGroupClient
+ IdentityClient *mock.MockIdentityClient
+ ImageClient *mock.MockImageClient
+ KeyPairClient *mock.MockKeyPairClient
+ LoadBalancerClient *mock.MockLoadBalancerClient
+ NetworkClient *mock.MockNetworkClient
+ RoleClient *mock.MockRoleClient
+ ServiceClient *mock.MockServiceClient
+ VolumeClient *mock.MockVolumeClient
+ VolumeTypeClient *mock.MockVolumeTypeClient
clientScopeCreateError error
}
@@ -61,19 +62,21 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory {
serviceClient := mock.NewMockServiceClient(mockCtrl)
volumeClient := mock.NewMockVolumeClient(mockCtrl)
volumetypeClient := mock.NewMockVolumeTypeClient(mockCtrl)
+ loadBalancerClient := mock.NewMockLoadBalancerClient(mockCtrl)
return &MockScopeFactory{
- ComputeClient: computeClient,
- DomainClient: domainClient,
- GroupClient: groupClient,
- IdentityClient: identityClient,
- ImageClient: imageClient,
- KeyPairClient: keypairClient,
- NetworkClient: networkClient,
- RoleClient: roleClient,
- ServiceClient: serviceClient,
- VolumeClient: volumeClient,
- VolumeTypeClient: volumetypeClient,
+ ComputeClient: computeClient,
+ DomainClient: domainClient,
+ GroupClient: groupClient,
+ IdentityClient: identityClient,
+ ImageClient: imageClient,
+ KeyPairClient: keypairClient,
+ LoadBalancerClient: loadBalancerClient,
+ NetworkClient: networkClient,
+ RoleClient: roleClient,
+ ServiceClient: serviceClient,
+ VolumeClient: volumeClient,
+ VolumeTypeClient: volumetypeClient,
}
}
@@ -132,6 +135,10 @@ func (f *MockScopeFactory) NewRoleClient() (osclients.RoleClient, error) {
return f.RoleClient, nil
}
+func (f *MockScopeFactory) NewLoadBalancerClient() (osclients.LoadBalancerClient, error) {
+ return f.LoadBalancerClient, nil
+}
+
func (f *MockScopeFactory) ExtractToken() (*tokens.Token, error) {
return &tokens.Token{ExpiresAt: time.Now().Add(24 * time.Hour)}, nil
}
diff --git a/internal/scope/provider.go b/internal/scope/provider.go
index 65670ba60..f1cf1a682 100644
--- a/internal/scope/provider.go
+++ b/internal/scope/provider.go
@@ -181,6 +181,10 @@ func (s *providerScope) NewRoleClient() (clients.RoleClient, error) {
return clients.NewRoleClient(s.providerClient, s.providerClientOpts)
}
+func (s *providerScope) NewLoadBalancerClient() (clients.LoadBalancerClient, error) {
+ return clients.NewLoadBalancerClient(s.providerClient, s.providerClientOpts)
+}
+
func (s *providerScope) ExtractToken() (*tokens.Token, error) {
client, err := openstack.NewIdentityV3(s.providerClient, gophercloud.EndpointOpts{})
if err != nil {
diff --git a/internal/scope/scope.go b/internal/scope/scope.go
index 7da50dc8f..27d799b88 100644
--- a/internal/scope/scope.go
+++ b/internal/scope/scope.go
@@ -59,6 +59,7 @@ type Scope interface {
NewServiceClient() (osclients.ServiceClient, error)
NewVolumeClient() (osclients.VolumeClient, error)
NewVolumeTypeClient() (osclients.VolumeTypeClient, error)
+ NewLoadBalancerClient() (osclients.LoadBalancerClient, error)
ExtractToken() (*tokens.Token, error)
}
diff --git a/kuttl-test.yaml b/kuttl-test.yaml
index d499782e6..b7adad88e 100644
--- a/kuttl-test.yaml
+++ b/kuttl-test.yaml
@@ -8,6 +8,7 @@ testDirs:
- ./internal/controllers/group/tests/
- ./internal/controllers/image/tests/
- ./internal/controllers/keypair/tests/
+- ./internal/controllers/loadbalancer/tests/
- ./internal/controllers/network/tests/
- ./internal/controllers/port/tests/
- ./internal/controllers/project/tests/
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancer.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancer.go
new file mode 100644
index 000000000..0fb8d2f2d
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancer.go
@@ -0,0 +1,281 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ internal "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/internal"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ managedfields "k8s.io/apimachinery/pkg/util/managedfields"
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// LoadBalancerApplyConfiguration represents a declarative configuration of the LoadBalancer type for use
+// with apply.
+type LoadBalancerApplyConfiguration struct {
+ v1.TypeMetaApplyConfiguration `json:",inline"`
+ *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
+ Spec *LoadBalancerSpecApplyConfiguration `json:"spec,omitempty"`
+ Status *LoadBalancerStatusApplyConfiguration `json:"status,omitempty"`
+}
+
+// LoadBalancer constructs a declarative configuration of the LoadBalancer type for use with
+// apply.
+func LoadBalancer(name, namespace string) *LoadBalancerApplyConfiguration {
+ b := &LoadBalancerApplyConfiguration{}
+ b.WithName(name)
+ b.WithNamespace(namespace)
+ b.WithKind("LoadBalancer")
+ b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1")
+ return b
+}
+
+// ExtractLoadBalancer extracts the applied configuration owned by fieldManager from
+// loadBalancer. If no managedFields are found in loadBalancer for fieldManager, a
+// LoadBalancerApplyConfiguration is returned with only the Name, Namespace (if applicable),
+// APIVersion and Kind populated. It is possible that no managed fields were found for because other
+// field managers have taken ownership of all the fields previously owned by fieldManager, or because
+// the fieldManager never owned fields any fields.
+// loadBalancer must be a unmodified LoadBalancer API object that was retrieved from the Kubernetes API.
+// ExtractLoadBalancer provides a way to perform a extract/modify-in-place/apply workflow.
+// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously
+// applied if another fieldManager has updated or force applied any of the previously applied fields.
+// Experimental!
+func ExtractLoadBalancer(loadBalancer *apiv1alpha1.LoadBalancer, fieldManager string) (*LoadBalancerApplyConfiguration, error) {
+ return extractLoadBalancer(loadBalancer, fieldManager, "")
+}
+
+// ExtractLoadBalancerStatus is the same as ExtractLoadBalancer except
+// that it extracts the status subresource applied configuration.
+// Experimental!
+func ExtractLoadBalancerStatus(loadBalancer *apiv1alpha1.LoadBalancer, fieldManager string) (*LoadBalancerApplyConfiguration, error) {
+ return extractLoadBalancer(loadBalancer, fieldManager, "status")
+}
+
+func extractLoadBalancer(loadBalancer *apiv1alpha1.LoadBalancer, fieldManager string, subresource string) (*LoadBalancerApplyConfiguration, error) {
+ b := &LoadBalancerApplyConfiguration{}
+ err := managedfields.ExtractInto(loadBalancer, internal.Parser().Type("com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancer"), fieldManager, b, subresource)
+ if err != nil {
+ return nil, err
+ }
+ b.WithName(loadBalancer.Name)
+ b.WithNamespace(loadBalancer.Namespace)
+
+ b.WithKind("LoadBalancer")
+ b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1")
+ return b, nil
+}
+func (b LoadBalancerApplyConfiguration) IsApplyConfiguration() {}
+
+// WithKind sets the Kind 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 Kind field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithKind(value string) *LoadBalancerApplyConfiguration {
+ b.TypeMetaApplyConfiguration.Kind = &value
+ return b
+}
+
+// WithAPIVersion sets the APIVersion 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 APIVersion field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithAPIVersion(value string) *LoadBalancerApplyConfiguration {
+ b.TypeMetaApplyConfiguration.APIVersion = &value
+ return b
+}
+
+// WithName sets the Name 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 Name field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithName(value string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Name = &value
+ return b
+}
+
+// WithGenerateName sets the GenerateName 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 GenerateName field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithGenerateName(value string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.GenerateName = &value
+ return b
+}
+
+// WithNamespace sets the Namespace 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 Namespace field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithNamespace(value string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Namespace = &value
+ return b
+}
+
+// WithUID sets the UID 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 UID field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithUID(value types.UID) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.UID = &value
+ return b
+}
+
+// WithResourceVersion sets the ResourceVersion 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 ResourceVersion field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithResourceVersion(value string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.ResourceVersion = &value
+ return b
+}
+
+// WithGeneration sets the Generation 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 Generation field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithGeneration(value int64) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Generation = &value
+ return b
+}
+
+// WithCreationTimestamp sets the CreationTimestamp 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 CreationTimestamp field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithCreationTimestamp(value metav1.Time) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
+ return b
+}
+
+// WithDeletionTimestamp sets the DeletionTimestamp 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 DeletionTimestamp field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
+ return b
+}
+
+// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds 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 DeletionGracePeriodSeconds field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
+ return b
+}
+
+// WithLabels puts the entries into the Labels field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Labels field,
+// overwriting an existing map entries in Labels field with the same key.
+func (b *LoadBalancerApplyConfiguration) WithLabels(entries map[string]string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Labels[k] = v
+ }
+ return b
+}
+
+// WithAnnotations puts the entries into the Annotations field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Annotations field,
+// overwriting an existing map entries in Annotations field with the same key.
+func (b *LoadBalancerApplyConfiguration) WithAnnotations(entries map[string]string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Annotations[k] = v
+ }
+ return b
+}
+
+// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
+func (b *LoadBalancerApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithOwnerReferences")
+ }
+ b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
+ }
+ return b
+}
+
+// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Finalizers field.
+func (b *LoadBalancerApplyConfiguration) WithFinalizers(values ...string) *LoadBalancerApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
+ }
+ return b
+}
+
+func (b *LoadBalancerApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
+ if b.ObjectMetaApplyConfiguration == nil {
+ b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
+ }
+}
+
+// WithSpec sets the Spec 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 Spec field is set to the value of the last call.
+func (b *LoadBalancerApplyConfiguration) WithSpec(value *LoadBalancerSpecApplyConfiguration) *LoadBalancerApplyConfiguration {
+ b.Spec = value
+ 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 *LoadBalancerApplyConfiguration) WithStatus(value *LoadBalancerStatusApplyConfiguration) *LoadBalancerApplyConfiguration {
+ b.Status = value
+ return b
+}
+
+// GetKind retrieves the value of the Kind field in the declarative configuration.
+func (b *LoadBalancerApplyConfiguration) GetKind() *string {
+ return b.TypeMetaApplyConfiguration.Kind
+}
+
+// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
+func (b *LoadBalancerApplyConfiguration) GetAPIVersion() *string {
+ return b.TypeMetaApplyConfiguration.APIVersion
+}
+
+// GetName retrieves the value of the Name field in the declarative configuration.
+func (b *LoadBalancerApplyConfiguration) GetName() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Name
+}
+
+// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
+func (b *LoadBalancerApplyConfiguration) GetNamespace() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Namespace
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerfilter.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerfilter.go
new file mode 100644
index 000000000..d5c92a62e
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerfilter.go
@@ -0,0 +1,159 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// LoadBalancerFilterApplyConfiguration represents a declarative configuration of the LoadBalancerFilter type for use
+// with apply.
+type LoadBalancerFilterApplyConfiguration struct {
+ Name *apiv1alpha1.OpenStackName `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ ProjectRef *apiv1alpha1.KubernetesNameRef `json:"projectRef,omitempty"`
+ VipSubnetRef *apiv1alpha1.KubernetesNameRef `json:"vipSubnetRef,omitempty"`
+ VipNetworkRef *apiv1alpha1.KubernetesNameRef `json:"vipNetworkRef,omitempty"`
+ VipPortRef *apiv1alpha1.KubernetesNameRef `json:"vipPortRef,omitempty"`
+ AvailabilityZone *string `json:"availabilityZone,omitempty"`
+ Provider *string `json:"provider,omitempty"`
+ VipAddress *string `json:"vipAddress,omitempty"`
+ Tags []apiv1alpha1.LoadBalancerTag `json:"tags,omitempty"`
+ TagsAny []apiv1alpha1.LoadBalancerTag `json:"tagsAny,omitempty"`
+ NotTags []apiv1alpha1.LoadBalancerTag `json:"notTags,omitempty"`
+ NotTagsAny []apiv1alpha1.LoadBalancerTag `json:"notTagsAny,omitempty"`
+}
+
+// LoadBalancerFilterApplyConfiguration constructs a declarative configuration of the LoadBalancerFilter type for use with
+// apply.
+func LoadBalancerFilter() *LoadBalancerFilterApplyConfiguration {
+ return &LoadBalancerFilterApplyConfiguration{}
+}
+
+// WithName sets the Name 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 Name field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithName(value apiv1alpha1.OpenStackName) *LoadBalancerFilterApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
+// WithDescription sets the Description 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 Description field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithDescription(value string) *LoadBalancerFilterApplyConfiguration {
+ b.Description = &value
+ return b
+}
+
+// WithProjectRef sets the ProjectRef 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 ProjectRef field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithProjectRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerFilterApplyConfiguration {
+ b.ProjectRef = &value
+ return b
+}
+
+// WithVipSubnetRef sets the VipSubnetRef 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 VipSubnetRef field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithVipSubnetRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerFilterApplyConfiguration {
+ b.VipSubnetRef = &value
+ return b
+}
+
+// WithVipNetworkRef sets the VipNetworkRef 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 VipNetworkRef field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithVipNetworkRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerFilterApplyConfiguration {
+ b.VipNetworkRef = &value
+ return b
+}
+
+// WithVipPortRef sets the VipPortRef 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 VipPortRef field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithVipPortRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerFilterApplyConfiguration {
+ b.VipPortRef = &value
+ return b
+}
+
+// WithAvailabilityZone sets the AvailabilityZone 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 AvailabilityZone field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithAvailabilityZone(value string) *LoadBalancerFilterApplyConfiguration {
+ b.AvailabilityZone = &value
+ return b
+}
+
+// WithProvider sets the Provider 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 Provider field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithProvider(value string) *LoadBalancerFilterApplyConfiguration {
+ b.Provider = &value
+ return b
+}
+
+// WithVipAddress sets the VipAddress 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 VipAddress field is set to the value of the last call.
+func (b *LoadBalancerFilterApplyConfiguration) WithVipAddress(value string) *LoadBalancerFilterApplyConfiguration {
+ b.VipAddress = &value
+ return b
+}
+
+// WithTags adds the given value to the Tags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Tags field.
+func (b *LoadBalancerFilterApplyConfiguration) WithTags(values ...apiv1alpha1.LoadBalancerTag) *LoadBalancerFilterApplyConfiguration {
+ for i := range values {
+ b.Tags = append(b.Tags, values[i])
+ }
+ return b
+}
+
+// WithTagsAny adds the given value to the TagsAny field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the TagsAny field.
+func (b *LoadBalancerFilterApplyConfiguration) WithTagsAny(values ...apiv1alpha1.LoadBalancerTag) *LoadBalancerFilterApplyConfiguration {
+ for i := range values {
+ b.TagsAny = append(b.TagsAny, values[i])
+ }
+ return b
+}
+
+// WithNotTags adds the given value to the NotTags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the NotTags field.
+func (b *LoadBalancerFilterApplyConfiguration) WithNotTags(values ...apiv1alpha1.LoadBalancerTag) *LoadBalancerFilterApplyConfiguration {
+ for i := range values {
+ b.NotTags = append(b.NotTags, values[i])
+ }
+ return b
+}
+
+// WithNotTagsAny adds the given value to the NotTagsAny field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the NotTagsAny field.
+func (b *LoadBalancerFilterApplyConfiguration) WithNotTagsAny(values ...apiv1alpha1.LoadBalancerTag) *LoadBalancerFilterApplyConfiguration {
+ for i := range values {
+ b.NotTagsAny = append(b.NotTagsAny, values[i])
+ }
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerimport.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerimport.go
new file mode 100644
index 000000000..df3c50e17
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerimport.go
@@ -0,0 +1,48 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+// LoadBalancerImportApplyConfiguration represents a declarative configuration of the LoadBalancerImport type for use
+// with apply.
+type LoadBalancerImportApplyConfiguration struct {
+ ID *string `json:"id,omitempty"`
+ Filter *LoadBalancerFilterApplyConfiguration `json:"filter,omitempty"`
+}
+
+// LoadBalancerImportApplyConfiguration constructs a declarative configuration of the LoadBalancerImport type for use with
+// apply.
+func LoadBalancerImport() *LoadBalancerImportApplyConfiguration {
+ return &LoadBalancerImportApplyConfiguration{}
+}
+
+// WithID sets the ID 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 ID field is set to the value of the last call.
+func (b *LoadBalancerImportApplyConfiguration) WithID(value string) *LoadBalancerImportApplyConfiguration {
+ b.ID = &value
+ return b
+}
+
+// WithFilter sets the Filter 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 Filter field is set to the value of the last call.
+func (b *LoadBalancerImportApplyConfiguration) WithFilter(value *LoadBalancerFilterApplyConfiguration) *LoadBalancerImportApplyConfiguration {
+ b.Filter = value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerresourcespec.go
new file mode 100644
index 000000000..064e29e66
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerresourcespec.go
@@ -0,0 +1,144 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// LoadBalancerResourceSpecApplyConfiguration represents a declarative configuration of the LoadBalancerResourceSpec type for use
+// with apply.
+type LoadBalancerResourceSpecApplyConfiguration struct {
+ Name *apiv1alpha1.OpenStackName `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ VipSubnetRef *apiv1alpha1.KubernetesNameRef `json:"vipSubnetRef,omitempty"`
+ VipNetworkRef *apiv1alpha1.KubernetesNameRef `json:"vipNetworkRef,omitempty"`
+ VipPortRef *apiv1alpha1.KubernetesNameRef `json:"vipPortRef,omitempty"`
+ FlavorRef *apiv1alpha1.KubernetesNameRef `json:"flavorRef,omitempty"`
+ ProjectRef *apiv1alpha1.KubernetesNameRef `json:"projectRef,omitempty"`
+ AdminStateUp *bool `json:"adminStateUp,omitempty"`
+ AvailabilityZone *string `json:"availabilityZone,omitempty"`
+ Provider *string `json:"provider,omitempty"`
+ VipAddress *apiv1alpha1.IPvAny `json:"vipAddress,omitempty"`
+ Tags []apiv1alpha1.LoadBalancerTag `json:"tags,omitempty"`
+}
+
+// LoadBalancerResourceSpecApplyConfiguration constructs a declarative configuration of the LoadBalancerResourceSpec type for use with
+// apply.
+func LoadBalancerResourceSpec() *LoadBalancerResourceSpecApplyConfiguration {
+ return &LoadBalancerResourceSpecApplyConfiguration{}
+}
+
+// WithName sets the Name 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 Name field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithName(value apiv1alpha1.OpenStackName) *LoadBalancerResourceSpecApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
+// WithDescription sets the Description 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 Description field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithDescription(value string) *LoadBalancerResourceSpecApplyConfiguration {
+ b.Description = &value
+ return b
+}
+
+// WithVipSubnetRef sets the VipSubnetRef 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 VipSubnetRef field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithVipSubnetRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerResourceSpecApplyConfiguration {
+ b.VipSubnetRef = &value
+ return b
+}
+
+// WithVipNetworkRef sets the VipNetworkRef 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 VipNetworkRef field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithVipNetworkRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerResourceSpecApplyConfiguration {
+ b.VipNetworkRef = &value
+ return b
+}
+
+// WithVipPortRef sets the VipPortRef 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 VipPortRef field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithVipPortRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerResourceSpecApplyConfiguration {
+ b.VipPortRef = &value
+ return b
+}
+
+// WithFlavorRef sets the FlavorRef 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 FlavorRef field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithFlavorRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerResourceSpecApplyConfiguration {
+ b.FlavorRef = &value
+ return b
+}
+
+// WithProjectRef sets the ProjectRef 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 ProjectRef field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithProjectRef(value apiv1alpha1.KubernetesNameRef) *LoadBalancerResourceSpecApplyConfiguration {
+ b.ProjectRef = &value
+ return b
+}
+
+// WithAdminStateUp sets the AdminStateUp 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 AdminStateUp field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithAdminStateUp(value bool) *LoadBalancerResourceSpecApplyConfiguration {
+ b.AdminStateUp = &value
+ return b
+}
+
+// WithAvailabilityZone sets the AvailabilityZone 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 AvailabilityZone field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithAvailabilityZone(value string) *LoadBalancerResourceSpecApplyConfiguration {
+ b.AvailabilityZone = &value
+ return b
+}
+
+// WithProvider sets the Provider 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 Provider field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithProvider(value string) *LoadBalancerResourceSpecApplyConfiguration {
+ b.Provider = &value
+ return b
+}
+
+// WithVipAddress sets the VipAddress 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 VipAddress field is set to the value of the last call.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithVipAddress(value apiv1alpha1.IPvAny) *LoadBalancerResourceSpecApplyConfiguration {
+ b.VipAddress = &value
+ return b
+}
+
+// WithTags adds the given value to the Tags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Tags field.
+func (b *LoadBalancerResourceSpecApplyConfiguration) WithTags(values ...apiv1alpha1.LoadBalancerTag) *LoadBalancerResourceSpecApplyConfiguration {
+ for i := range values {
+ b.Tags = append(b.Tags, values[i])
+ }
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerresourcestatus.go
new file mode 100644
index 000000000..9da0447f7
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerresourcestatus.go
@@ -0,0 +1,158 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+// LoadBalancerResourceStatusApplyConfiguration represents a declarative configuration of the LoadBalancerResourceStatus type for use
+// with apply.
+type LoadBalancerResourceStatusApplyConfiguration struct {
+ Name *string `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ VipSubnetID *string `json:"vipSubnetID,omitempty"`
+ VipNetworkID *string `json:"vipNetworkID,omitempty"`
+ VipPortID *string `json:"vipPortID,omitempty"`
+ FlavorID *string `json:"flavorID,omitempty"`
+ ProjectID *string `json:"projectID,omitempty"`
+ AdminStateUp *bool `json:"adminStateUp,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ AvailabilityZone *string `json:"availabilityZone,omitempty"`
+ ProvisioningStatus *string `json:"provisioningStatus,omitempty"`
+ OperatingStatus *string `json:"operatingStatus,omitempty"`
+ Provider *string `json:"provider,omitempty"`
+ VipAddress *string `json:"vipAddress,omitempty"`
+}
+
+// LoadBalancerResourceStatusApplyConfiguration constructs a declarative configuration of the LoadBalancerResourceStatus type for use with
+// apply.
+func LoadBalancerResourceStatus() *LoadBalancerResourceStatusApplyConfiguration {
+ return &LoadBalancerResourceStatusApplyConfiguration{}
+}
+
+// WithName sets the Name 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 Name field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithName(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
+// WithDescription sets the Description 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 Description field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithDescription(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.Description = &value
+ return b
+}
+
+// WithVipSubnetID sets the VipSubnetID 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 VipSubnetID field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithVipSubnetID(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.VipSubnetID = &value
+ return b
+}
+
+// WithVipNetworkID sets the VipNetworkID 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 VipNetworkID field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithVipNetworkID(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.VipNetworkID = &value
+ return b
+}
+
+// WithVipPortID sets the VipPortID 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 VipPortID field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithVipPortID(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.VipPortID = &value
+ return b
+}
+
+// WithFlavorID sets the FlavorID 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 FlavorID field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithFlavorID(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.FlavorID = &value
+ return b
+}
+
+// WithProjectID sets the ProjectID 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 ProjectID field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithProjectID(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.ProjectID = &value
+ return b
+}
+
+// WithAdminStateUp sets the AdminStateUp 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 AdminStateUp field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithAdminStateUp(value bool) *LoadBalancerResourceStatusApplyConfiguration {
+ b.AdminStateUp = &value
+ return b
+}
+
+// WithTags adds the given value to the Tags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Tags field.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithTags(values ...string) *LoadBalancerResourceStatusApplyConfiguration {
+ for i := range values {
+ b.Tags = append(b.Tags, values[i])
+ }
+ return b
+}
+
+// WithAvailabilityZone sets the AvailabilityZone 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 AvailabilityZone field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithAvailabilityZone(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.AvailabilityZone = &value
+ return b
+}
+
+// WithProvisioningStatus sets the ProvisioningStatus 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 ProvisioningStatus field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithProvisioningStatus(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.ProvisioningStatus = &value
+ return b
+}
+
+// WithOperatingStatus sets the OperatingStatus 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 OperatingStatus field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithOperatingStatus(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.OperatingStatus = &value
+ return b
+}
+
+// WithProvider sets the Provider 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 Provider field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithProvider(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.Provider = &value
+ return b
+}
+
+// WithVipAddress sets the VipAddress 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 VipAddress field is set to the value of the last call.
+func (b *LoadBalancerResourceStatusApplyConfiguration) WithVipAddress(value string) *LoadBalancerResourceStatusApplyConfiguration {
+ b.VipAddress = &value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerspec.go
new file mode 100644
index 000000000..db531a018
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerspec.go
@@ -0,0 +1,79 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// LoadBalancerSpecApplyConfiguration represents a declarative configuration of the LoadBalancerSpec type for use
+// with apply.
+type LoadBalancerSpecApplyConfiguration struct {
+ Import *LoadBalancerImportApplyConfiguration `json:"import,omitempty"`
+ Resource *LoadBalancerResourceSpecApplyConfiguration `json:"resource,omitempty"`
+ ManagementPolicy *apiv1alpha1.ManagementPolicy `json:"managementPolicy,omitempty"`
+ ManagedOptions *ManagedOptionsApplyConfiguration `json:"managedOptions,omitempty"`
+ CloudCredentialsRef *CloudCredentialsReferenceApplyConfiguration `json:"cloudCredentialsRef,omitempty"`
+}
+
+// LoadBalancerSpecApplyConfiguration constructs a declarative configuration of the LoadBalancerSpec type for use with
+// apply.
+func LoadBalancerSpec() *LoadBalancerSpecApplyConfiguration {
+ return &LoadBalancerSpecApplyConfiguration{}
+}
+
+// WithImport sets the Import 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 Import field is set to the value of the last call.
+func (b *LoadBalancerSpecApplyConfiguration) WithImport(value *LoadBalancerImportApplyConfiguration) *LoadBalancerSpecApplyConfiguration {
+ b.Import = value
+ return b
+}
+
+// WithResource sets the Resource 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 Resource field is set to the value of the last call.
+func (b *LoadBalancerSpecApplyConfiguration) WithResource(value *LoadBalancerResourceSpecApplyConfiguration) *LoadBalancerSpecApplyConfiguration {
+ b.Resource = value
+ return b
+}
+
+// WithManagementPolicy sets the ManagementPolicy 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 ManagementPolicy field is set to the value of the last call.
+func (b *LoadBalancerSpecApplyConfiguration) WithManagementPolicy(value apiv1alpha1.ManagementPolicy) *LoadBalancerSpecApplyConfiguration {
+ b.ManagementPolicy = &value
+ return b
+}
+
+// WithManagedOptions sets the ManagedOptions 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 ManagedOptions field is set to the value of the last call.
+func (b *LoadBalancerSpecApplyConfiguration) WithManagedOptions(value *ManagedOptionsApplyConfiguration) *LoadBalancerSpecApplyConfiguration {
+ b.ManagedOptions = value
+ return b
+}
+
+// WithCloudCredentialsRef sets the CloudCredentialsRef 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 CloudCredentialsRef field is set to the value of the last call.
+func (b *LoadBalancerSpecApplyConfiguration) WithCloudCredentialsRef(value *CloudCredentialsReferenceApplyConfiguration) *LoadBalancerSpecApplyConfiguration {
+ b.CloudCredentialsRef = value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerstatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerstatus.go
new file mode 100644
index 000000000..28c977f47
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/loadbalancerstatus.go
@@ -0,0 +1,66 @@
+/*
+Copyright The ORC 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 v1alpha1
+
+import (
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// LoadBalancerStatusApplyConfiguration represents a declarative configuration of the LoadBalancerStatus type for use
+// with apply.
+type LoadBalancerStatusApplyConfiguration struct {
+ Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
+ ID *string `json:"id,omitempty"`
+ Resource *LoadBalancerResourceStatusApplyConfiguration `json:"resource,omitempty"`
+}
+
+// LoadBalancerStatusApplyConfiguration constructs a declarative configuration of the LoadBalancerStatus type for use with
+// apply.
+func LoadBalancerStatus() *LoadBalancerStatusApplyConfiguration {
+ return &LoadBalancerStatusApplyConfiguration{}
+}
+
+// WithConditions adds the given value to the Conditions field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Conditions field.
+func (b *LoadBalancerStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *LoadBalancerStatusApplyConfiguration {
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithConditions")
+ }
+ b.Conditions = append(b.Conditions, *values[i])
+ }
+ return b
+}
+
+// WithID sets the ID 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 ID field is set to the value of the last call.
+func (b *LoadBalancerStatusApplyConfiguration) WithID(value string) *LoadBalancerStatusApplyConfiguration {
+ b.ID = &value
+ return b
+}
+
+// WithResource sets the Resource 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 Resource field is set to the value of the last call.
+func (b *LoadBalancerStatusApplyConfiguration) WithResource(value *LoadBalancerResourceStatusApplyConfiguration) *LoadBalancerStatusApplyConfiguration {
+ b.Resource = value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go
index abaeca27f..e42093a7b 100644
--- a/pkg/clients/applyconfiguration/internal/internal.go
+++ b/pkg/clients/applyconfiguration/internal/internal.go
@@ -998,6 +998,216 @@ var schemaYAML = typed.YAMLObject(`types:
- name: resource
type:
namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.KeyPairResourceStatus
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancer
+ map:
+ fields:
+ - name: apiVersion
+ type:
+ scalar: string
+ - name: kind
+ type:
+ scalar: string
+ - name: metadata
+ type:
+ namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta
+ default: {}
+ - name: spec
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerSpec
+ default: {}
+ - name: status
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerStatus
+ default: {}
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerFilter
+ map:
+ fields:
+ - name: availabilityZone
+ type:
+ scalar: string
+ - name: description
+ type:
+ scalar: string
+ - name: name
+ type:
+ scalar: string
+ - name: notTags
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: notTagsAny
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: projectRef
+ type:
+ scalar: string
+ - name: provider
+ type:
+ scalar: string
+ - name: tags
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: tagsAny
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: vipAddress
+ type:
+ scalar: string
+ - name: vipNetworkRef
+ type:
+ scalar: string
+ - name: vipPortRef
+ type:
+ scalar: string
+ - name: vipSubnetRef
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerImport
+ map:
+ fields:
+ - name: filter
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerFilter
+ - name: id
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerResourceSpec
+ map:
+ fields:
+ - name: adminStateUp
+ type:
+ scalar: boolean
+ - name: availabilityZone
+ type:
+ scalar: string
+ - name: description
+ type:
+ scalar: string
+ - name: flavorRef
+ type:
+ scalar: string
+ - name: name
+ type:
+ scalar: string
+ - name: projectRef
+ type:
+ scalar: string
+ - name: provider
+ type:
+ scalar: string
+ - name: tags
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: associative
+ - name: vipAddress
+ type:
+ scalar: string
+ - name: vipNetworkRef
+ type:
+ scalar: string
+ - name: vipPortRef
+ type:
+ scalar: string
+ - name: vipSubnetRef
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerResourceStatus
+ map:
+ fields:
+ - name: adminStateUp
+ type:
+ scalar: boolean
+ - name: availabilityZone
+ type:
+ scalar: string
+ - name: description
+ type:
+ scalar: string
+ - name: flavorID
+ type:
+ scalar: string
+ - name: name
+ type:
+ scalar: string
+ - name: operatingStatus
+ type:
+ scalar: string
+ - name: projectID
+ type:
+ scalar: string
+ - name: provider
+ type:
+ scalar: string
+ - name: provisioningStatus
+ type:
+ scalar: string
+ - name: tags
+ type:
+ list:
+ elementType:
+ scalar: string
+ elementRelationship: atomic
+ - name: vipAddress
+ type:
+ scalar: string
+ - name: vipNetworkID
+ type:
+ scalar: string
+ - name: vipPortID
+ type:
+ scalar: string
+ - name: vipSubnetID
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerSpec
+ map:
+ fields:
+ - name: cloudCredentialsRef
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.CloudCredentialsReference
+ default: {}
+ - name: import
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerImport
+ - name: managedOptions
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ManagedOptions
+ - name: managementPolicy
+ type:
+ scalar: string
+ - name: resource
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerResourceSpec
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerStatus
+ map:
+ fields:
+ - name: conditions
+ type:
+ list:
+ elementType:
+ namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Condition
+ elementRelationship: associative
+ keys:
+ - type
+ - name: id
+ type:
+ scalar: string
+ - name: resource
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LoadBalancerResourceStatus
- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ManagedOptions
map:
fields:
diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go
index 5a3990951..f16dccfdc 100644
--- a/pkg/clients/applyconfiguration/utils.go
+++ b/pkg/clients/applyconfiguration/utils.go
@@ -160,6 +160,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &apiv1alpha1.KeyPairSpecApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("KeyPairStatus"):
return &apiv1alpha1.KeyPairStatusApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancer"):
+ return &apiv1alpha1.LoadBalancerApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancerFilter"):
+ return &apiv1alpha1.LoadBalancerFilterApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancerImport"):
+ return &apiv1alpha1.LoadBalancerImportApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancerResourceSpec"):
+ return &apiv1alpha1.LoadBalancerResourceSpecApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancerResourceStatus"):
+ return &apiv1alpha1.LoadBalancerResourceStatusApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancerSpec"):
+ return &apiv1alpha1.LoadBalancerSpecApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("LoadBalancerStatus"):
+ return &apiv1alpha1.LoadBalancerStatusApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("ManagedOptions"):
return &apiv1alpha1.ManagedOptionsApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Network"):
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go
index 4317c8aa6..2dd876e2f 100644
--- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go
@@ -34,6 +34,7 @@ type OpenstackV1alpha1Interface interface {
GroupsGetter
ImagesGetter
KeyPairsGetter
+ LoadBalancersGetter
NetworksGetter
PortsGetter
ProjectsGetter
@@ -78,6 +79,10 @@ func (c *OpenstackV1alpha1Client) KeyPairs(namespace string) KeyPairInterface {
return newKeyPairs(c, namespace)
}
+func (c *OpenstackV1alpha1Client) LoadBalancers(namespace string) LoadBalancerInterface {
+ return newLoadBalancers(c, namespace)
+}
+
func (c *OpenstackV1alpha1Client) Networks(namespace string) NetworkInterface {
return newNetworks(c, namespace)
}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go
index 595446f05..51690611e 100644
--- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go
@@ -52,6 +52,10 @@ func (c *FakeOpenstackV1alpha1) KeyPairs(namespace string) v1alpha1.KeyPairInter
return newFakeKeyPairs(c, namespace)
}
+func (c *FakeOpenstackV1alpha1) LoadBalancers(namespace string) v1alpha1.LoadBalancerInterface {
+ return newFakeLoadBalancers(c, namespace)
+}
+
func (c *FakeOpenstackV1alpha1) Networks(namespace string) v1alpha1.NetworkInterface {
return newFakeNetworks(c, namespace)
}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_loadbalancer.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_loadbalancer.go
new file mode 100644
index 000000000..8e50b630e
--- /dev/null
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_loadbalancer.go
@@ -0,0 +1,53 @@
+/*
+Copyright The ORC 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 client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ v1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+ typedapiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset/typed/api/v1alpha1"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// fakeLoadBalancers implements LoadBalancerInterface
+type fakeLoadBalancers struct {
+ *gentype.FakeClientWithListAndApply[*v1alpha1.LoadBalancer, *v1alpha1.LoadBalancerList, *apiv1alpha1.LoadBalancerApplyConfiguration]
+ Fake *FakeOpenstackV1alpha1
+}
+
+func newFakeLoadBalancers(fake *FakeOpenstackV1alpha1, namespace string) typedapiv1alpha1.LoadBalancerInterface {
+ return &fakeLoadBalancers{
+ gentype.NewFakeClientWithListAndApply[*v1alpha1.LoadBalancer, *v1alpha1.LoadBalancerList, *apiv1alpha1.LoadBalancerApplyConfiguration](
+ fake.Fake,
+ namespace,
+ v1alpha1.SchemeGroupVersion.WithResource("loadbalancers"),
+ v1alpha1.SchemeGroupVersion.WithKind("LoadBalancer"),
+ func() *v1alpha1.LoadBalancer { return &v1alpha1.LoadBalancer{} },
+ func() *v1alpha1.LoadBalancerList { return &v1alpha1.LoadBalancerList{} },
+ func(dst, src *v1alpha1.LoadBalancerList) { dst.ListMeta = src.ListMeta },
+ func(list *v1alpha1.LoadBalancerList) []*v1alpha1.LoadBalancer {
+ return gentype.ToPointerSlice(list.Items)
+ },
+ func(list *v1alpha1.LoadBalancerList, items []*v1alpha1.LoadBalancer) {
+ list.Items = gentype.FromPointerSlice(items)
+ },
+ ),
+ fake,
+ }
+}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go
index 558e399d0..9b9ea5c5b 100644
--- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go
@@ -30,6 +30,8 @@ type ImageExpansion interface{}
type KeyPairExpansion interface{}
+type LoadBalancerExpansion interface{}
+
type NetworkExpansion interface{}
type PortExpansion interface{}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/loadbalancer.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/loadbalancer.go
new file mode 100644
index 000000000..9437a4493
--- /dev/null
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/loadbalancer.go
@@ -0,0 +1,74 @@
+/*
+Copyright The ORC 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 client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ applyconfigurationapiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+ scheme "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset/scheme"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// LoadBalancersGetter has a method to return a LoadBalancerInterface.
+// A group's client should implement this interface.
+type LoadBalancersGetter interface {
+ LoadBalancers(namespace string) LoadBalancerInterface
+}
+
+// LoadBalancerInterface has methods to work with LoadBalancer resources.
+type LoadBalancerInterface interface {
+ Create(ctx context.Context, loadBalancer *apiv1alpha1.LoadBalancer, opts v1.CreateOptions) (*apiv1alpha1.LoadBalancer, error)
+ Update(ctx context.Context, loadBalancer *apiv1alpha1.LoadBalancer, opts v1.UpdateOptions) (*apiv1alpha1.LoadBalancer, error)
+ // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+ UpdateStatus(ctx context.Context, loadBalancer *apiv1alpha1.LoadBalancer, opts v1.UpdateOptions) (*apiv1alpha1.LoadBalancer, 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) (*apiv1alpha1.LoadBalancer, error)
+ List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.LoadBalancerList, error)
+ 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 *apiv1alpha1.LoadBalancer, err error)
+ Apply(ctx context.Context, loadBalancer *applyconfigurationapiv1alpha1.LoadBalancerApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.LoadBalancer, err error)
+ // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus().
+ ApplyStatus(ctx context.Context, loadBalancer *applyconfigurationapiv1alpha1.LoadBalancerApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.LoadBalancer, err error)
+ LoadBalancerExpansion
+}
+
+// loadBalancers implements LoadBalancerInterface
+type loadBalancers struct {
+ *gentype.ClientWithListAndApply[*apiv1alpha1.LoadBalancer, *apiv1alpha1.LoadBalancerList, *applyconfigurationapiv1alpha1.LoadBalancerApplyConfiguration]
+}
+
+// newLoadBalancers returns a LoadBalancers
+func newLoadBalancers(c *OpenstackV1alpha1Client, namespace string) *loadBalancers {
+ return &loadBalancers{
+ gentype.NewClientWithListAndApply[*apiv1alpha1.LoadBalancer, *apiv1alpha1.LoadBalancerList, *applyconfigurationapiv1alpha1.LoadBalancerApplyConfiguration](
+ "loadbalancers",
+ c.RESTClient(),
+ scheme.ParameterCodec,
+ namespace,
+ func() *apiv1alpha1.LoadBalancer { return &apiv1alpha1.LoadBalancer{} },
+ func() *apiv1alpha1.LoadBalancerList { return &apiv1alpha1.LoadBalancerList{} },
+ ),
+ }
+}
diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go
index 2e06781ab..44ac917d4 100644
--- a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go
+++ b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go
@@ -36,6 +36,8 @@ type Interface interface {
Images() ImageInformer
// KeyPairs returns a KeyPairInformer.
KeyPairs() KeyPairInformer
+ // LoadBalancers returns a LoadBalancerInformer.
+ LoadBalancers() LoadBalancerInformer
// Networks returns a NetworkInformer.
Networks() NetworkInformer
// Ports returns a PortInformer.
@@ -105,6 +107,11 @@ func (v *version) KeyPairs() KeyPairInformer {
return &keyPairInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}
+// LoadBalancers returns a LoadBalancerInformer.
+func (v *version) LoadBalancers() LoadBalancerInformer {
+ return &loadBalancerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
// Networks returns a NetworkInformer.
func (v *version) Networks() NetworkInformer {
return &networkInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/loadbalancer.go b/pkg/clients/informers/externalversions/api/v1alpha1/loadbalancer.go
new file mode 100644
index 000000000..d47c37881
--- /dev/null
+++ b/pkg/clients/informers/externalversions/api/v1alpha1/loadbalancer.go
@@ -0,0 +1,102 @@
+/*
+Copyright The ORC 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 informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+ time "time"
+
+ v2apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ clientset "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset"
+ internalinterfaces "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/informers/externalversions/internalinterfaces"
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/listers/api/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ watch "k8s.io/apimachinery/pkg/watch"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// LoadBalancerInformer provides access to a shared informer and lister for
+// LoadBalancers.
+type LoadBalancerInformer interface {
+ Informer() cache.SharedIndexInformer
+ Lister() apiv1alpha1.LoadBalancerLister
+}
+
+type loadBalancerInformer struct {
+ factory internalinterfaces.SharedInformerFactory
+ tweakListOptions internalinterfaces.TweakListOptionsFunc
+ namespace string
+}
+
+// NewLoadBalancerInformer constructs a new informer for LoadBalancer type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewLoadBalancerInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+ return NewFilteredLoadBalancerInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredLoadBalancerInformer constructs a new informer for LoadBalancer type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredLoadBalancerInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+ return cache.NewSharedIndexInformer(
+ &cache.ListWatch{
+ ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().LoadBalancers(namespace).List(context.Background(), options)
+ },
+ WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().LoadBalancers(namespace).Watch(context.Background(), options)
+ },
+ ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().LoadBalancers(namespace).List(ctx, options)
+ },
+ WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().LoadBalancers(namespace).Watch(ctx, options)
+ },
+ },
+ &v2apiv1alpha1.LoadBalancer{},
+ resyncPeriod,
+ indexers,
+ )
+}
+
+func (f *loadBalancerInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+ return NewFilteredLoadBalancerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *loadBalancerInformer) Informer() cache.SharedIndexInformer {
+ return f.factory.InformerFor(&v2apiv1alpha1.LoadBalancer{}, f.defaultInformer)
+}
+
+func (f *loadBalancerInformer) Lister() apiv1alpha1.LoadBalancerLister {
+ return apiv1alpha1.NewLoadBalancerLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/clients/informers/externalversions/generic.go b/pkg/clients/informers/externalversions/generic.go
index 1bb2313e0..2a0d4a54c 100644
--- a/pkg/clients/informers/externalversions/generic.go
+++ b/pkg/clients/informers/externalversions/generic.go
@@ -65,6 +65,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Images().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("keypairs"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().KeyPairs().Informer()}, nil
+ case v1alpha1.SchemeGroupVersion.WithResource("loadbalancers"):
+ return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().LoadBalancers().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("networks"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Networks().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("ports"):
diff --git a/pkg/clients/listers/api/v1alpha1/expansion_generated.go b/pkg/clients/listers/api/v1alpha1/expansion_generated.go
index 1380d0372..a5baf2ac8 100644
--- a/pkg/clients/listers/api/v1alpha1/expansion_generated.go
+++ b/pkg/clients/listers/api/v1alpha1/expansion_generated.go
@@ -66,6 +66,14 @@ type KeyPairListerExpansion interface{}
// KeyPairNamespaceLister.
type KeyPairNamespaceListerExpansion interface{}
+// LoadBalancerListerExpansion allows custom methods to be added to
+// LoadBalancerLister.
+type LoadBalancerListerExpansion interface{}
+
+// LoadBalancerNamespaceListerExpansion allows custom methods to be added to
+// LoadBalancerNamespaceLister.
+type LoadBalancerNamespaceListerExpansion interface{}
+
// NetworkListerExpansion allows custom methods to be added to
// NetworkLister.
type NetworkListerExpansion interface{}
diff --git a/pkg/clients/listers/api/v1alpha1/loadbalancer.go b/pkg/clients/listers/api/v1alpha1/loadbalancer.go
new file mode 100644
index 000000000..f231564f8
--- /dev/null
+++ b/pkg/clients/listers/api/v1alpha1/loadbalancer.go
@@ -0,0 +1,70 @@
+/*
+Copyright The ORC 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 lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ listers "k8s.io/client-go/listers"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// LoadBalancerLister helps list LoadBalancers.
+// All objects returned here must be treated as read-only.
+type LoadBalancerLister interface {
+ // List lists all LoadBalancers in the indexer.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*apiv1alpha1.LoadBalancer, err error)
+ // LoadBalancers returns an object that can list and get LoadBalancers.
+ LoadBalancers(namespace string) LoadBalancerNamespaceLister
+ LoadBalancerListerExpansion
+}
+
+// loadBalancerLister implements the LoadBalancerLister interface.
+type loadBalancerLister struct {
+ listers.ResourceIndexer[*apiv1alpha1.LoadBalancer]
+}
+
+// NewLoadBalancerLister returns a new LoadBalancerLister.
+func NewLoadBalancerLister(indexer cache.Indexer) LoadBalancerLister {
+ return &loadBalancerLister{listers.New[*apiv1alpha1.LoadBalancer](indexer, apiv1alpha1.Resource("loadbalancer"))}
+}
+
+// LoadBalancers returns an object that can list and get LoadBalancers.
+func (s *loadBalancerLister) LoadBalancers(namespace string) LoadBalancerNamespaceLister {
+ return loadBalancerNamespaceLister{listers.NewNamespaced[*apiv1alpha1.LoadBalancer](s.ResourceIndexer, namespace)}
+}
+
+// LoadBalancerNamespaceLister helps list and get LoadBalancers.
+// All objects returned here must be treated as read-only.
+type LoadBalancerNamespaceLister interface {
+ // List lists all LoadBalancers in the indexer for a given namespace.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*apiv1alpha1.LoadBalancer, err error)
+ // Get retrieves the LoadBalancer from the indexer for a given namespace and name.
+ // Objects returned here must be treated as read-only.
+ Get(name string) (*apiv1alpha1.LoadBalancer, error)
+ LoadBalancerNamespaceListerExpansion
+}
+
+// loadBalancerNamespaceLister implements the LoadBalancerNamespaceLister
+// interface.
+type loadBalancerNamespaceLister struct {
+ listers.ResourceIndexer[*apiv1alpha1.LoadBalancer]
+}
diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md
index 32018f962..5196dc15b 100644
--- a/website/docs/crd-reference.md
+++ b/website/docs/crd-reference.md
@@ -16,6 +16,7 @@ Package v1alpha1 contains API Schema definitions for the openstack v1alpha1 API
- [Group](#group)
- [Image](#image)
- [KeyPair](#keypair)
+- [LoadBalancer](#loadbalancer)
- [Network](#network)
- [Port](#port)
- [Project](#project)
@@ -169,6 +170,7 @@ _Appears in:_
- [GroupSpec](#groupspec)
- [ImageSpec](#imagespec)
- [KeyPairSpec](#keypairspec)
+- [LoadBalancerSpec](#loadbalancerspec)
- [NetworkSpec](#networkspec)
- [PortSpec](#portspec)
- [ProjectSpec](#projectspec)
@@ -1005,6 +1007,7 @@ _Appears in:_
- [FloatingIPFilter](#floatingipfilter)
- [FloatingIPResourceSpec](#floatingipresourcespec)
- [HostRoute](#hostroute)
+- [LoadBalancerResourceSpec](#loadbalancerresourcespec)
- [SubnetFilter](#subnetfilter)
- [SubnetGateway](#subnetgateway)
- [SubnetResourceSpec](#subnetresourcespec)
@@ -1616,6 +1619,8 @@ _Appears in:_
- [FloatingIPResourceSpec](#floatingipresourcespec)
- [GroupFilter](#groupfilter)
- [GroupResourceSpec](#groupresourcespec)
+- [LoadBalancerFilter](#loadbalancerfilter)
+- [LoadBalancerResourceSpec](#loadbalancerresourcespec)
- [NetworkFilter](#networkfilter)
- [NetworkResourceSpec](#networkresourcespec)
- [PortFilter](#portfilter)
@@ -1637,6 +1642,184 @@ _Appears in:_
+#### LoadBalancer
+
+
+
+LoadBalancer is the Schema for an ORC resource.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `openstack.k-orc.cloud/v1alpha1` | | |
+| `kind` _string_ | `LoadBalancer` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[LoadBalancerSpec](#loadbalancerspec)_ | spec specifies the desired state of the resource. | | |
+| `status` _[LoadBalancerStatus](#loadbalancerstatus)_ | status defines the observed state of the resource. | | |
+
+
+#### LoadBalancerFilter
+
+
+
+LoadBalancerFilter defines an existing resource by its properties
+
+_Validation:_
+- MinProperties: 1
+
+_Appears in:_
+- [LoadBalancerImport](#loadbalancerimport)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _[OpenStackName](#openstackname)_ | name of the existing resource | | MaxLength: 255
MinLength: 1
Pattern: `^[^,]+$`
|
+| `description` _string_ | description of the existing resource | | MaxLength: 255
MinLength: 1
|
+| `projectRef` _[KubernetesNameRef](#kubernetesnameref)_ | projectRef is a reference to the ORC Project this resource is associated with.
Typically, only used by admin. | | MaxLength: 253
MinLength: 1
|
+| `vipSubnetRef` _[KubernetesNameRef](#kubernetesnameref)_ | vipSubnetRef filters by the subnet on which the load balancer's address is allocated. | | MaxLength: 253
MinLength: 1
|
+| `vipNetworkRef` _[KubernetesNameRef](#kubernetesnameref)_ | vipNetworkRef filters by the network on which the load balancer's address is allocated. | | MaxLength: 253
MinLength: 1
|
+| `vipPortRef` _[KubernetesNameRef](#kubernetesnameref)_ | vipPortRef filters by the neutron port used for the VIP. | | MaxLength: 253
MinLength: 1
|
+| `availabilityZone` _string_ | availabilityZone is the availability zone in which to create the load balancer. | | MaxLength: 255
|
+| `provider` _string_ | provider filters by the name of the load balancer provider. | | MaxLength: 255
|
+| `vipAddress` _string_ | vipAddress filters by the IP address of the load balancer's VIP. | | MaxLength: 64
|
+| `tags` _[LoadBalancerTag](#loadbalancertag) array_ | tags is a list of tags to filter by. If specified, the resource must
have all of the tags specified to be included in the result. | | MaxItems: 64
MaxLength: 255
MinLength: 1
|
+| `tagsAny` _[LoadBalancerTag](#loadbalancertag) array_ | tagsAny is a list of tags to filter by. If specified, the resource
must have at least one of the tags specified to be included in the
result. | | MaxItems: 64
MaxLength: 255
MinLength: 1
|
+| `notTags` _[LoadBalancerTag](#loadbalancertag) array_ | notTags is a list of tags to filter by. If specified, resources which
contain all of the given tags will be excluded from the result. | | MaxItems: 64
MaxLength: 255
MinLength: 1
|
+| `notTagsAny` _[LoadBalancerTag](#loadbalancertag) array_ | notTagsAny is a list of tags to filter by. If specified, resources
which contain any of the given tags will be excluded from the result. | | MaxItems: 64
MaxLength: 255
MinLength: 1
|
+
+
+#### LoadBalancerImport
+
+
+
+LoadBalancerImport specifies an existing resource which will be imported instead of
+creating a new one
+
+_Validation:_
+- MaxProperties: 1
+- MinProperties: 1
+
+_Appears in:_
+- [LoadBalancerSpec](#loadbalancerspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `id` _string_ | id contains the unique identifier of an existing OpenStack resource. Note
that when specifying an import by ID, the resource MUST already exist.
The ORC object will enter an error state if the resource does not exist. | | Format: uuid
|
+| `filter` _[LoadBalancerFilter](#loadbalancerfilter)_ | filter contains a resource query which is expected to return a single
result. The controller will continue to retry if filter returns no
results. If filter returns multiple results the controller will set an
error state and will not continue to retry. | | MinProperties: 1
|
+
+
+#### LoadBalancerResourceSpec
+
+
+
+LoadBalancerResourceSpec contains the desired state of the resource.
+
+
+
+_Appears in:_
+- [LoadBalancerSpec](#loadbalancerspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _[OpenStackName](#openstackname)_ | name will be the name of the created resource. If not specified, the
name of the ORC object will be used. | | MaxLength: 255
MinLength: 1
Pattern: `^[^,]+$`
|
+| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 255
MinLength: 1
|
+| `vipSubnetRef` _[KubernetesNameRef](#kubernetesnameref)_ | vipSubnetRef is the subnet on which to allocate the load balancer's address. | | MaxLength: 253
MinLength: 1
|
+| `vipNetworkRef` _[KubernetesNameRef](#kubernetesnameref)_ | vipNetworkRef is the network on which to allocate the load balancer's address. | | MaxLength: 253
MinLength: 1
|
+| `vipPortRef` _[KubernetesNameRef](#kubernetesnameref)_ | vipPortRef is a reference to a neutron port to use for the VIP. If the port
has more than one subnet you must specify either vipSubnetRef or vipAddress
to clarify which address should be used for the VIP. | | MaxLength: 253
MinLength: 1
|
+| `flavorRef` _[KubernetesNameRef](#kubernetesnameref)_ | flavorRef is a reference to the ORC Flavor which this resource is associated with. | | MaxLength: 253
MinLength: 1
|
+| `projectRef` _[KubernetesNameRef](#kubernetesnameref)_ | projectRef is a reference to the ORC Project which this resource is associated with. | | MaxLength: 253
MinLength: 1
|
+| `adminStateUp` _boolean_ | adminStateUp is the administrative state of the load balancer, which is up (true) or down (false) | | |
+| `availabilityZone` _string_ | availabilityZone is the availability zone in which to create the load balancer. | | MaxLength: 255
|
+| `provider` _string_ | provider is the name of the load balancer provider. | | MaxLength: 255
|
+| `vipAddress` _[IPvAny](#ipvany)_ | vipAddress is the specific IP address to use for the VIP (optional).
If not specified, one is allocated automatically from the subnet. | | MaxLength: 45
MinLength: 1
|
+| `tags` _[LoadBalancerTag](#loadbalancertag) array_ | tags is a list of tags which will be applied to the load balancer. | | MaxItems: 64
MaxLength: 255
MinLength: 1
|
+
+
+#### LoadBalancerResourceStatus
+
+
+
+LoadBalancerResourceStatus represents the observed state of the resource.
+
+
+
+_Appears in:_
+- [LoadBalancerStatus](#loadbalancerstatus)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _string_ | name is a Human-readable name for the resource. Might not be unique. | | MaxLength: 1024
|
+| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 1024
|
+| `vipSubnetID` _string_ | vipSubnetID is the ID of the Subnet to which the resource is associated. | | MaxLength: 1024
|
+| `vipNetworkID` _string_ | vipNetworkID is the ID of the Network to which the resource is associated. | | MaxLength: 1024
|
+| `vipPortID` _string_ | vipPortID is the ID of the Port to which the resource is associated. | | MaxLength: 1024
|
+| `flavorID` _string_ | flavorID is the ID of the Flavor to which the resource is associated. | | MaxLength: 1024
|
+| `projectID` _string_ | projectID is the ID of the Project to which the resource is associated. | | MaxLength: 1024
|
+| `adminStateUp` _boolean_ | adminStateUp is the administrative state of the load balancer,
which is up (true) or down (false). | | |
+| `tags` _string array_ | tags is the list of tags on the resource. | | MaxItems: 64
items:MaxLength: 255
|
+| `availabilityZone` _string_ | availabilityZone is the availability zone where the load balancer is located. | | MaxLength: 1024
|
+| `provisioningStatus` _string_ | provisioningStatus is the provisioning status of the load balancer.
This value is ACTIVE, PENDING_CREATE or ERROR. | | MaxLength: 1024
|
+| `operatingStatus` _string_ | operatingStatus is the operating status of the load balancer,
such as ONLINE or OFFLINE. | | MaxLength: 1024
|
+| `provider` _string_ | provider is the name of the load balancer provider. | | MaxLength: 1024
|
+| `vipAddress` _string_ | vipAddress is the IP address of the load balancer's VIP. | | MaxLength: 64
|
+
+
+#### LoadBalancerSpec
+
+
+
+LoadBalancerSpec defines the desired state of an ORC object.
+
+
+
+_Appears in:_
+- [LoadBalancer](#loadbalancer)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `import` _[LoadBalancerImport](#loadbalancerimport)_ | import refers to an existing OpenStack resource which will be imported instead of
creating a new one. | | MaxProperties: 1
MinProperties: 1
|
+| `resource` _[LoadBalancerResourceSpec](#loadbalancerresourcespec)_ | resource specifies the desired state of the resource.
resource may not be specified if the management policy is `unmanaged`.
resource must be specified if the management policy is `managed`. | | |
+| `managementPolicy` _[ManagementPolicy](#managementpolicy)_ | managementPolicy defines how ORC will treat the object. Valid values are
`managed`: ORC will create, update, and delete the resource; `unmanaged`:
ORC will import an existing resource, and will not apply updates to it or
delete it. | managed | Enum: [managed unmanaged]
|
+| `managedOptions` _[ManagedOptions](#managedoptions)_ | managedOptions specifies options which may be applied to managed objects. | | |
+| `cloudCredentialsRef` _[CloudCredentialsReference](#cloudcredentialsreference)_ | cloudCredentialsRef points to a secret containing OpenStack credentials | | |
+
+
+#### LoadBalancerStatus
+
+
+
+LoadBalancerStatus defines the observed state of an ORC resource.
+
+
+
+_Appears in:_
+- [LoadBalancer](#loadbalancer)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta) array_ | conditions represents the observed status of the object.
Known .status.conditions.type are: "Available", "Progressing"
Available represents the availability of the OpenStack resource. If it is
true then the resource is ready for use.
Progressing indicates whether the controller is still attempting to
reconcile the current state of the OpenStack resource to the desired
state. Progressing will be False either because the desired state has
been achieved, or because some terminal error prevents it from ever being
achieved and the controller is no longer attempting to reconcile. If
Progressing is True, an observer waiting on the resource should continue
to wait. | | MaxItems: 32
|
+| `id` _string_ | id is the unique identifier of the OpenStack resource. | | |
+| `resource` _[LoadBalancerResourceStatus](#loadbalancerresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | |
+
+
+#### LoadBalancerTag
+
+_Underlying type:_ _string_
+
+
+
+_Validation:_
+- MaxLength: 255
+- MinLength: 1
+
+_Appears in:_
+- [LoadBalancerFilter](#loadbalancerfilter)
+- [LoadBalancerResourceSpec](#loadbalancerresourcespec)
+
+
+
#### MAC
_Underlying type:_ _string_
@@ -1682,6 +1865,7 @@ _Appears in:_
- [GroupSpec](#groupspec)
- [ImageSpec](#imagespec)
- [KeyPairSpec](#keypairspec)
+- [LoadBalancerSpec](#loadbalancerspec)
- [NetworkSpec](#networkspec)
- [PortSpec](#portspec)
- [ProjectSpec](#projectspec)
@@ -1716,6 +1900,7 @@ _Appears in:_
- [GroupSpec](#groupspec)
- [ImageSpec](#imagespec)
- [KeyPairSpec](#keypairspec)
+- [LoadBalancerSpec](#loadbalancerspec)
- [NetworkSpec](#networkspec)
- [PortSpec](#portspec)
- [ProjectSpec](#projectspec)
@@ -2009,6 +2194,8 @@ _Appears in:_
- [ImageResourceSpec](#imageresourcespec)
- [KeyPairFilter](#keypairfilter)
- [KeyPairResourceSpec](#keypairresourcespec)
+- [LoadBalancerFilter](#loadbalancerfilter)
+- [LoadBalancerResourceSpec](#loadbalancerresourcespec)
- [NetworkFilter](#networkfilter)
- [NetworkResourceSpec](#networkresourcespec)
- [PortFilter](#portfilter)
diff --git a/website/docs/design/dependency-resolver.md b/website/docs/design/dependency-resolver.md
new file mode 100644
index 000000000..fcfb510cf
--- /dev/null
+++ b/website/docs/design/dependency-resolver.md
@@ -0,0 +1,391 @@
+# Dependency Resolver Builder Pattern
+
+## Problem Statement
+
+Controllers frequently need to resolve `*KubernetesNameRef` fields to their OpenStack IDs. This results in repetitive boilerplate code across controllers:
+
+```go
+var networkID string
+if filter.NetworkRef != nil {
+ networkKey := client.ObjectKey{Name: string(*filter.NetworkRef), Namespace: obj.Namespace}
+ if err := actuator.k8sClient.Get(ctx, networkKey, network); err != nil {
+ if apierrors.IsNotFound(err) {
+ reconcileStatus = reconcileStatus.WithReconcileStatus(
+ progress.WaitingOnObject("Network", networkKey.Name, progress.WaitingOnCreation))
+ } else {
+ reconcileStatus = reconcileStatus.WithReconcileStatus(
+ progress.WrapError(fmt.Errorf("fetching network %s: %w", networkKey.Name, err)))
+ }
+ } else {
+ if !orcv1alpha1.IsAvailable(network) || network.Status.ID == nil {
+ reconcileStatus = reconcileStatus.WithReconcileStatus(
+ progress.WaitingOnObject("Network", networkKey.Name, progress.WaitingOnReady))
+ } else {
+ networkID = *network.Status.ID
+ }
+ }
+}
+// Repeat for Subnet, Project, Port, etc...
+```
+
+This pattern is repeated across:
+- `ListOSResourcesForImport` (resolving filter refs)
+- `CreateResource` (resolving spec refs)
+- Various reconcilers
+
+## Proposed Solution
+
+A builder-pattern resolver that:
+1. Chains multiple dependency resolutions
+2. Accumulates errors and wait states
+3. Returns resolved IDs in a type-safe way
+4. Provides clear, readable code
+
+## API Design
+
+### Core Types
+
+```go
+package dependency
+
+// Resolver accumulates dependency resolutions and their statuses
+type Resolver struct {
+ ctx context.Context
+ k8sClient client.Client
+ namespace string
+ reconcileStatus progress.ReconcileStatus
+ resolved map[string]string // kind -> ID
+}
+
+// NewResolver creates a new dependency resolver
+func NewResolver(ctx context.Context, k8sClient client.Client, namespace string) *Resolver
+
+// Optional resolves a ref if it's non-nil, skips if nil
+// T is the ORC resource type (e.g., *orcv1alpha1.Network)
+func (r *Resolver) Optional[TP DependencyType[T], T any](
+ ref *KubernetesNameRef,
+ kind string,
+ getID func(TP) *string,
+) *Resolver
+
+// Required resolves a ref that must exist (adds error if nil)
+func (r *Resolver) Required[TP DependencyType[T], T any](
+ ref KubernetesNameRef,
+ kind string,
+ getID func(TP) *string,
+) *Resolver
+
+// Result returns the resolved IDs and accumulated status
+func (r *Resolver) Result() (ResolvedDependencies, progress.ReconcileStatus)
+
+// ResolvedDependencies provides type-safe access to resolved IDs
+type ResolvedDependencies struct {
+ ids map[string]string
+}
+
+func (r ResolvedDependencies) Get(kind string) string
+func (r ResolvedDependencies) GetPtr(kind string) *string
+```
+
+### Usage Examples
+
+#### Example 1: ListOSResourcesForImport (Filter Resolution)
+
+**Before:**
+```go
+func (actuator floatingipCreateActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
+ var reconcileStatus progress.ReconcileStatus
+
+ network := &orcv1alpha1.Network{}
+ if filter.FloatingNetworkRef != nil {
+ networkKey := client.ObjectKey{Name: string(ptr.Deref(filter.FloatingNetworkRef, "")), Namespace: obj.Namespace}
+ if err := actuator.k8sClient.Get(ctx, networkKey, network); err != nil {
+ // ... 15 lines of error handling
+ }
+ }
+
+ port := &orcv1alpha1.Port{}
+ if filter.PortRef != nil {
+ // ... another 15 lines
+ }
+
+ project := &orcv1alpha1.Project{}
+ if filter.ProjectRef != nil {
+ // ... another 15 lines
+ }
+
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return nil, reconcileStatus
+ }
+
+ listOpts := floatingips.ListOpts{
+ PortID: ptr.Deref(port.Status.ID, ""),
+ FloatingNetworkID: ptr.Deref(network.Status.ID, ""),
+ ProjectID: ptr.Deref(project.Status.ID, ""),
+ // ...
+ }
+ // ...
+}
+```
+
+**After:**
+```go
+func (actuator floatingipCreateActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
+ deps, rs := dependency.NewResolver(ctx, actuator.k8sClient, obj.Namespace).
+ Optional[*orcv1alpha1.Network](filter.FloatingNetworkRef, "Network", func(n *orcv1alpha1.Network) *string { return n.Status.ID }).
+ Optional[*orcv1alpha1.Port](filter.PortRef, "Port", func(p *orcv1alpha1.Port) *string { return p.Status.ID }).
+ Optional[*orcv1alpha1.Project](filter.ProjectRef, "Project", func(p *orcv1alpha1.Project) *string { return p.Status.ID }).
+ Result()
+
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, rs
+ }
+
+ listOpts := floatingips.ListOpts{
+ PortID: deps.Get("Port"),
+ FloatingNetworkID: deps.Get("Network"),
+ ProjectID: deps.Get("Project"),
+ // ...
+ }
+ // ...
+}
+```
+
+#### Example 2: CreateResource (Spec Resolution)
+
+**Before:**
+```go
+func (actuator loadbalancerActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) {
+ resource := obj.Spec.Resource
+ var reconcileStatus progress.ReconcileStatus
+
+ var vipSubnetID string
+ if resource.VipSubnetRef != nil {
+ subnet, subnetDepRS := subnetDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Subnet) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(subnetDepRS)
+ if subnet != nil {
+ vipSubnetID = ptr.Deref(subnet.Status.ID, "")
+ }
+ }
+
+ var vipNetworkID string
+ if resource.VipNetworkRef != nil {
+ // ... repeat pattern
+ }
+
+ // ... more dependencies
+}
+```
+
+**After:**
+```go
+func (actuator loadbalancerActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) {
+ resource := obj.Spec.Resource
+
+ deps, rs := dependency.NewResolver(ctx, actuator.k8sClient, obj.Namespace).
+ Optional[*orcv1alpha1.Subnet](resource.VipSubnetRef, "Subnet", func(s *orcv1alpha1.Subnet) *string { return s.Status.ID }).
+ Optional[*orcv1alpha1.Network](resource.VipNetworkRef, "Network", func(n *orcv1alpha1.Network) *string { return n.Status.ID }).
+ Optional[*orcv1alpha1.Port](resource.VipPortRef, "Port", func(p *orcv1alpha1.Port) *string { return p.Status.ID }).
+ Optional[*orcv1alpha1.Flavor](resource.FlavorRef, "Flavor", func(f *orcv1alpha1.Flavor) *string { return f.Status.ID }).
+ Optional[*orcv1alpha1.Project](resource.ProjectRef, "Project", func(p *orcv1alpha1.Project) *string { return p.Status.ID }).
+ Result()
+
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, rs
+ }
+
+ createOpts := loadbalancers.CreateOpts{
+ VipSubnetID: deps.Get("Subnet"),
+ VipNetworkID: deps.Get("Network"),
+ VipPortID: deps.Get("Port"),
+ FlavorID: deps.Get("Flavor"),
+ ProjectID: deps.Get("Project"),
+ // ...
+ }
+ // ...
+}
+```
+
+## Implementation
+
+### File: `internal/util/dependency/resolver.go`
+
+```go
+package dependency
+
+import (
+ "context"
+ "fmt"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
+)
+
+// Resolver accumulates dependency resolutions and their statuses.
+type Resolver struct {
+ ctx context.Context
+ k8sClient client.Client
+ namespace string
+ reconcileStatus progress.ReconcileStatus
+ resolved map[string]string
+}
+
+// NewResolver creates a new dependency resolver for the given namespace.
+func NewResolver(ctx context.Context, k8sClient client.Client, namespace string) *Resolver {
+ return &Resolver{
+ ctx: ctx,
+ k8sClient: k8sClient,
+ namespace: namespace,
+ resolved: make(map[string]string),
+ }
+}
+
+// Optional resolves a ref if it's non-nil. If the ref is nil, it's skipped.
+// The kind parameter is used for error messages and as the key in resolved dependencies.
+// The getID function extracts the OpenStack ID from the resolved object.
+func Optional[TP DependencyType[T], T any](r *Resolver, ref *orcv1alpha1.KubernetesNameRef, kind string, getID func(TP) *string) *Resolver {
+ if ref == nil {
+ return r
+ }
+
+ name := string(*ref)
+ var obj TP = new(T)
+ objectKey := client.ObjectKey{Name: name, Namespace: r.namespace}
+
+ if err := r.k8sClient.Get(r.ctx, objectKey, obj); err != nil {
+ if apierrors.IsNotFound(err) {
+ r.reconcileStatus = r.reconcileStatus.WaitingOnObject(kind, name, progress.WaitingOnCreation)
+ } else {
+ r.reconcileStatus = r.reconcileStatus.WithError(fmt.Errorf("fetching %s %s: %w", kind, name, err))
+ }
+ return r
+ }
+
+ if !orcv1alpha1.IsAvailable(obj) {
+ r.reconcileStatus = r.reconcileStatus.WaitingOnObject(kind, name, progress.WaitingOnReady)
+ return r
+ }
+
+ id := getID(obj)
+ if id == nil {
+ r.reconcileStatus = r.reconcileStatus.WaitingOnObject(kind, name, progress.WaitingOnReady)
+ return r
+ }
+
+ r.resolved[kind] = *id
+ return r
+}
+
+// Required resolves a ref that must exist. If the ref is empty, an error is added.
+func Required[TP DependencyType[T], T any](r *Resolver, ref orcv1alpha1.KubernetesNameRef, kind string, getID func(TP) *string) *Resolver {
+ return Optional[TP, T](r, ptr.To(ref), kind, getID)
+}
+
+// Result returns the resolved dependencies and accumulated reconcile status.
+func (r *Resolver) Result() (ResolvedDependencies, progress.ReconcileStatus) {
+ return ResolvedDependencies{ids: r.resolved}, r.reconcileStatus
+}
+
+// ResolvedDependencies provides access to resolved OpenStack IDs.
+type ResolvedDependencies struct {
+ ids map[string]string
+}
+
+// Get returns the resolved ID for the given kind, or empty string if not resolved.
+func (r ResolvedDependencies) Get(kind string) string {
+ return r.ids[kind]
+}
+
+// GetPtr returns a pointer to the resolved ID, or nil if not resolved.
+func (r ResolvedDependencies) GetPtr(kind string) *string {
+ if id, ok := r.ids[kind]; ok {
+ return &id
+ }
+ return nil
+}
+
+// Has returns true if the kind was resolved.
+func (r ResolvedDependencies) Has(kind string) bool {
+ _, ok := r.ids[kind]
+ return ok
+}
+```
+
+### Alternative: Method Chaining with Generics
+
+Due to Go's limitation that methods cannot have type parameters, the fluent API requires top-level functions:
+
+```go
+// Usage with top-level functions (required by Go)
+deps, rs := dependency.Optional[*orcv1alpha1.Network](
+ dependency.Optional[*orcv1alpha1.Subnet](
+ dependency.NewResolver(ctx, k8sClient, namespace),
+ filter.SubnetRef, "Subnet", func(s *orcv1alpha1.Subnet) *string { return s.Status.ID },
+ ),
+ filter.NetworkRef, "Network", func(n *orcv1alpha1.Network) *string { return n.Status.ID },
+).Result()
+```
+
+This is less readable. A better alternative is a non-generic wrapper:
+
+```go
+// Resolver methods return *Resolver for chaining
+func (r *Resolver) OptionalNetwork(ref *KubernetesNameRef) *Resolver
+func (r *Resolver) OptionalSubnet(ref *KubernetesNameRef) *Resolver
+func (r *Resolver) OptionalProject(ref *KubernetesNameRef) *Resolver
+// ... one method per known type
+```
+
+This sacrifices genericity for readability but requires adding a method for each type.
+
+## Trade-offs
+
+### Pros
+- Significantly reduces boilerplate (45+ lines → 10 lines for 3 dependencies)
+- Consistent error handling across all controllers
+- Clear, readable code
+- Type-safe
+- No reflection magic
+
+### Cons
+- Go generics limitation: can't have generic methods, so either:
+ - Use top-level functions (less fluent)
+ - Add a method per type (more code in resolver)
+- Slightly more abstraction to understand
+
+## Relationship with Existing DeletionGuardDependency
+
+This resolver is complementary to `DeletionGuardDependency`:
+
+| Aspect | DeletionGuardDependency | Resolver |
+|--------|------------------------|----------|
+| **Purpose** | Full lifecycle management with finalizers | Simple one-off resolution |
+| **Finalizers** | Yes, prevents deletion | No |
+| **Watch/Index** | Yes, triggers reconciliation | No |
+| **Use case** | Spec refs (create/update) | Filter refs, quick lookups |
+
+For `CreateResource`, you might still want `DeletionGuardDependency.GetDependency()` if you need finalizer protection. The Resolver is best for:
+- `ListOSResourcesForImport` filter resolution
+- Quick lookups where finalizers aren't needed
+- Reducing boilerplate in any dependency fetching
+
+## Migration Path
+
+1. Add `Resolver` to `internal/util/dependency/resolver.go`
+2. Update one controller (e.g., floatingip) to use it
+3. Validate it works correctly
+4. Gradually migrate other controllers
+
+## Open Questions
+
+1. Should `Resolver` integrate with `DeletionGuardDependency` to add finalizers?
+2. Should there be pre-defined methods like `OptionalProject()` for common types?
+3. Should the ready check (`IsAvailable && Status.ID != nil`) be customizable per call?