From 5f338699c4211d674d21b84447b2d78b36ec42ef Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Tue, 23 Jun 2026 10:28:07 +0300 Subject: [PATCH 1/7] SwiftContainer: scaffold controller Scaffold the SwiftContainer controller using the controller generator: $ go run ./cmd/scaffold-controller -interactive=false -kind=SwiftContainer -gophercloud-client=NewObjectStorageV1 -gophercloud-module=github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers -gophercloud-type=Container -openstack-json-object=container --- api/v1alpha1/swiftcontainer_types.go | 74 ++++++ api/v1alpha1/zz_generated.deepcopy.go | 65 +++++ cmd/models-schema/zz_generated.openapi.go | 84 +++++++ config/rbac/role.yaml | 2 + .../openstack_v1alpha1_swiftcontainer.yaml | 14 ++ .../controllers/swiftcontainer/actuator.go | 238 ++++++++++++++++++ .../swiftcontainer/actuator_test.go | 119 +++++++++ .../controllers/swiftcontainer/controller.go | 68 +++++ internal/controllers/swiftcontainer/status.go | 63 +++++ .../swiftcontainer-create-full/00-assert.yaml | 28 +++ .../00-create-resource.yaml | 15 ++ .../swiftcontainer-create-full/00-secret.yaml | 6 + .../swiftcontainer-create-full/README.md | 11 + .../00-assert.yaml | 27 ++ .../00-create-resource.yaml | 14 ++ .../00-secret.yaml | 6 + .../01-assert.yaml | 11 + .../01-delete-secret.yaml | 7 + .../swiftcontainer-create-minimal/README.md | 15 ++ .../00-assert.yaml | 30 +++ .../00-create-resources.yaml | 28 +++ .../00-secret.yaml | 6 + .../01-assert.yaml | 15 ++ .../01-import-resource.yaml | 13 + .../swiftcontainer-import-error/README.md | 13 + .../swiftcontainer-import/00-assert.yaml | 15 ++ .../00-import-resource.yaml | 15 ++ .../swiftcontainer-import/00-secret.yaml | 6 + .../swiftcontainer-import/01-assert.yaml | 34 +++ .../01-create-trap-resource.yaml | 17 ++ .../swiftcontainer-import/02-assert.yaml | 33 +++ .../02-create-resource.yaml | 14 ++ .../tests/swiftcontainer-import/README.md | 18 ++ .../swiftcontainer-update/00-assert.yaml | 26 ++ .../00-minimal-resource.yaml | 14 ++ .../swiftcontainer-update/00-secret.yaml | 6 + .../swiftcontainer-update/01-assert.yaml | 17 ++ .../01-updated-resource.yaml | 10 + .../swiftcontainer-update/02-assert.yaml | 26 ++ .../02-reverted-resource.yaml | 7 + .../tests/swiftcontainer-update/README.md | 17 ++ internal/osclients/swiftcontainer.go | 104 ++++++++ test/apivalidations/swiftcontainer_test.go | 105 ++++++++ website/docs/crd-reference.md | 8 + 44 files changed, 1464 insertions(+) create mode 100644 api/v1alpha1/swiftcontainer_types.go create mode 100644 config/samples/openstack_v1alpha1_swiftcontainer.yaml create mode 100644 internal/controllers/swiftcontainer/actuator.go create mode 100644 internal/controllers/swiftcontainer/actuator_test.go create mode 100644 internal/controllers/swiftcontainer/controller.go create mode 100644 internal/controllers/swiftcontainer/status.go create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-secret.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md create mode 100644 internal/osclients/swiftcontainer.go create mode 100644 test/apivalidations/swiftcontainer_test.go diff --git a/api/v1alpha1/swiftcontainer_types.go b/api/v1alpha1/swiftcontainer_types.go new file mode 100644 index 000000000..5be3eb199 --- /dev/null +++ b/api/v1alpha1/swiftcontainer_types.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. +*/ + +package v1alpha1 + +// SwiftContainerResourceSpec contains the desired state of the resource. +type SwiftContainerResourceSpec 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"` + + // TODO(scaffolding): Add more types. + // To see what is supported, you can take inspiration from the CreateOpts structure from + // github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers + // + // Until you have implemented mutability for the field, you must add a CEL validation + // preventing the field being modified: + // `// +kubebuilder:validation:XValidation:rule="self == oldSelf",message=" is immutable"` +} + +// SwiftContainerFilter defines an existing resource by its properties +// +kubebuilder:validation:MinProperties:=1 +type SwiftContainerFilter 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"` + + // TODO(scaffolding): Add more types. + // To see what is supported, you can take inspiration from the ListOpts structure from + // github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers +} + +// SwiftContainerResourceStatus represents the observed state of the resource. +type SwiftContainerResourceStatus 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"` + + // TODO(scaffolding): Add more types. + // To see what is supported, you can take inspiration from the Container structure from + // github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ead3ef708..d802d007e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6183,6 +6183,71 @@ func (in *SubnetStatus) DeepCopy() *SubnetStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerFilter) DeepCopyInto(out *SwiftContainerFilter) { + *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 + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerFilter. +func (in *SwiftContainerFilter) DeepCopy() *SwiftContainerFilter { + if in == nil { + return nil + } + out := new(SwiftContainerFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerResourceSpec) DeepCopyInto(out *SwiftContainerResourceSpec) { + *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 + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerResourceSpec. +func (in *SwiftContainerResourceSpec) DeepCopy() *SwiftContainerResourceSpec { + if in == nil { + return nil + } + out := new(SwiftContainerResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerResourceStatus) DeepCopyInto(out *SwiftContainerResourceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerResourceStatus. +func (in *SwiftContainerResourceStatus) DeepCopy() *SwiftContainerResourceStatus { + if in == nil { + return nil + } + out := new(SwiftContainerResourceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Trunk) DeepCopyInto(out *Trunk) { *out = *in diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 5601a14eb..0a5bb558b 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -240,6 +240,9 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SubnetResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SubnetResourceStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SubnetSpec": schema_openstack_resource_controller_v2_api_v1alpha1_SubnetSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SubnetStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SubnetStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerFilter": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerFilter(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResourceSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResourceStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Trunk": schema_openstack_resource_controller_v2_api_v1alpha1_Trunk(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.TrunkFilter": schema_openstack_resource_controller_v2_api_v1alpha1_TrunkFilter(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.TrunkImport": schema_openstack_resource_controller_v2_api_v1alpha1_TrunkImport(ref), @@ -11743,6 +11746,87 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SubnetStatus(ref commo } } +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerFilter 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: "", + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerResourceSpec 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: "", + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerResourceStatus 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: "", + }, + }, + }, + }, + }, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_Trunk(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4991cff67..8d59943dd 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -38,6 +38,7 @@ rules: - services - sharenetworks - subnets + - swiftcontainers - trunks - users - volumes @@ -74,6 +75,7 @@ rules: - services/status - sharenetworks/status - subnets/status + - swiftcontainers/status - trunks/status - users/status - volumes/status diff --git a/config/samples/openstack_v1alpha1_swiftcontainer.yaml b/config/samples/openstack_v1alpha1_swiftcontainer.yaml new file mode 100644 index 000000000..7ed4f4bf9 --- /dev/null +++ b/config/samples/openstack_v1alpha1_swiftcontainer.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-sample +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Sample SwiftContainer + # TODO(scaffolding): Add all fields the resource supports diff --git a/internal/controllers/swiftcontainer/actuator.go b/internal/controllers/swiftcontainer/actuator.go new file mode 100644 index 000000000..91306cb93 --- /dev/null +++ b/internal/controllers/swiftcontainer/actuator.go @@ -0,0 +1,238 @@ +/* +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 swiftcontainer + +import ( + "context" + "iter" + + "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" + 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" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" +) + +// OpenStack resource types +type ( + osResourceT = containers.Container + + 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 swiftcontainerActuator struct { + osClient osclients.SwiftContainerClient + k8sClient client.Client +} + +var _ createResourceActuator = swiftcontainerActuator{} +var _ deleteResourceActuator = swiftcontainerActuator{} + +func (swiftcontainerActuator) GetResourceID(osResource *osResourceT) string { + return osResource.ID +} + +func (actuator swiftcontainerActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) { + resource, err := actuator.osClient.GetSwiftContainer(ctx, id) + if err != nil { + return nil, progress.WrapError(err) + } + return resource, nil +} + +func (actuator swiftcontainerActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) { + resourceSpec := orcObject.Spec.Resource + if resourceSpec == nil { + return nil, false + } + + // TODO(scaffolding) If you need to filter resources on fields that the List() function + // of gophercloud does not support, it's possible to perform client-side filtering. + // Check osclients.ResourceFilter + + listOpts := containers.ListOpts{ + Name: getResourceName(orcObject), + Description: ptr.Deref(resourceSpec.Description, ""), + } + + return actuator.osClient.ListSwiftContainers(ctx, listOpts), true +} + +func (actuator swiftcontainerActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) { + // TODO(scaffolding) If you need to filter resources on fields that the List() function + // of gophercloud does not support, it's possible to perform client-side filtering. + // Check osclients.ResourceFilter + + listOpts := containers.ListOpts{ + Name: string(ptr.Deref(filter.Name, "")), + Description: string(ptr.Deref(filter.Description, "")), + // TODO(scaffolding): Add more import filters + } + + return actuator.osClient.ListSwiftContainers(ctx, listOpts), nil +} + +func (actuator swiftcontainerActuator) 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")) + } + createOpts := containers.CreateOpts{ + Name: getResourceName(obj), + Description: ptr.Deref(resource.Description, ""), + // TODO(scaffolding): Add more fields + } + + osResource, err := actuator.osClient.CreateSwiftContainer(ctx, createOpts) + if err != nil { + 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 swiftcontainerActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus { + return progress.WrapError(actuator.osClient.DeleteSwiftContainer(ctx, resource.ID)) +} + +func (actuator swiftcontainerActuator) 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 := containers.UpdateOpts{} + + handleNameUpdate(&updateOpts, obj, osResource) + handleDescriptionUpdate(&updateOpts, resource, osResource) + + // TODO(scaffolding): add handler for all fields supporting mutability + + 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.UpdateSwiftContainer(ctx, osResource.ID, updateOpts) + + if err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err) + } + return progress.WrapError(err) + } + + return progress.NeedsRefresh() +} + +func needsUpdate(updateOpts containers.UpdateOpts) (bool, error) { + updateOptsMap, err := updateOpts.ToContainerUpdateMap() + if err != nil { + return false, err + } + + updateMap, ok := updateOptsMap["container"].(map[string]any) + if !ok { + updateMap = make(map[string]any) + } + + return len(updateMap) > 0, nil +} + +func handleNameUpdate(updateOpts *containers.UpdateOpts, obj orcObjectPT, osResource *osResourceT) { + name := getResourceName(obj) + if osResource.Name != name { + updateOpts.Name = &name + } +} + +func handleDescriptionUpdate(updateOpts *containers.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + description := ptr.Deref(resource.Description, "") + if osResource.Description != description { + updateOpts.Description = &description + } +} + +func (actuator swiftcontainerActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { + return []resourceReconciler{ + actuator.updateResource, + }, nil +} + +type swiftcontainerHelperFactory struct{} + +var _ helperFactory = swiftcontainerHelperFactory{} + +func newActuator(ctx context.Context, orcObject *orcv1alpha1.SwiftContainer, controller interfaces.ResourceController) (swiftcontainerActuator, 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 swiftcontainerActuator{}, reconcileStatus + } + + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) + if err != nil { + return swiftcontainerActuator{}, progress.WrapError(err) + } + osClient, err := clientScope.NewSwiftContainerClient() + if err != nil { + return swiftcontainerActuator{}, progress.WrapError(err) + } + + return swiftcontainerActuator{ + osClient: osClient, + k8sClient: controller.GetK8sClient(), + }, nil +} + +func (swiftcontainerHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { + return swiftcontainerAdapter{obj} +} + +func (swiftcontainerHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} + +func (swiftcontainerHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} diff --git a/internal/controllers/swiftcontainer/actuator_test.go b/internal/controllers/swiftcontainer/actuator_test.go new file mode 100644 index 000000000..3bbd72425 --- /dev/null +++ b/internal/controllers/swiftcontainer/actuator_test.go @@ -0,0 +1,119 @@ +/* +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 swiftcontainer + +import ( + "testing" + + "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" + 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 containers.UpdateOpts + expectChange bool + }{ + { + name: "Empty base opts", + updateOpts: containers.UpdateOpts{}, + expectChange: false, + }, + { + name: "Updated opts", + updateOpts: containers.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.SwiftContainer{} + resource.Name = "object-name" + resource.Spec = orcv1alpha1.SwiftContainerSpec{ + Resource: &orcv1alpha1.SwiftContainerResourceSpec{Name: tt.newValue}, + } + osResource := &osResourceT{Name: tt.existingValue} + + updateOpts := containers.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.SwiftContainerResourceSpec{Description: tt.newValue} + osResource := &osResourceT{Description: tt.existingValue} + + updateOpts := containers.UpdateOpts{} + handleDescriptionUpdate(&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/swiftcontainer/controller.go b/internal/controllers/swiftcontainer/controller.go new file mode 100644 index 000000000..8f3b759db --- /dev/null +++ b/internal/controllers/swiftcontainer/controller.go @@ -0,0 +1,68 @@ +/* +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 swiftcontainer + +import ( + "context" + "errors" + + ctrl "sigs.k8s.io/controller-runtime" + "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" +) + +const controllerName = "swiftcontainer" + +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=swiftcontainers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=swiftcontainers/status,verbs=get;update;patch + +type swiftcontainerReconcilerConstructor struct { + scopeFactory scope.Factory +} + +func New(scopeFactory scope.Factory) interfaces.Controller { + return swiftcontainerReconcilerConstructor{scopeFactory: scopeFactory} +} + +func (swiftcontainerReconcilerConstructor) GetName() string { + return controllerName +} + +// SetupWithManager sets up the controller with the Manager. +func (c swiftcontainerReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) + + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). + For(&orcv1alpha1.SwiftContainer{}) + + if err := errors.Join( + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), + ); err != nil { + return err + } + + r := reconciler.NewController(controllerName, mgr.GetClient(), c.scopeFactory, swiftcontainerHelperFactory{}, swiftcontainerStatusWriter{}) + return builder.Complete(&r) +} diff --git a/internal/controllers/swiftcontainer/status.go b/internal/controllers/swiftcontainer/status.go new file mode 100644 index 000000000..c467380b0 --- /dev/null +++ b/internal/controllers/swiftcontainer/status.go @@ -0,0 +1,63 @@ +/* +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 swiftcontainer + +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 swiftcontainerStatusWriter struct{} + +type objectApplyT = orcapplyconfigv1alpha1.SwiftContainerApplyConfiguration +type statusApplyT = orcapplyconfigv1alpha1.SwiftContainerStatusApplyConfiguration + +var _ interfaces.ResourceStatusWriter[*orcv1alpha1.SwiftContainer, *osResourceT, *objectApplyT, *statusApplyT] = swiftcontainerStatusWriter{} + +func (swiftcontainerStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT { + return orcapplyconfigv1alpha1.SwiftContainer(name, namespace) +} + +func (swiftcontainerStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.SwiftContainer, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) { + if osResource == nil { + if orcObject.Status.ID == nil { + return metav1.ConditionFalse, nil + } else { + return metav1.ConditionUnknown, nil + } + } + return metav1.ConditionTrue, nil +} + +func (swiftcontainerStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { + resourceStatus := orcapplyconfigv1alpha1.SwiftContainerResourceStatus(). + WithName(osResource.Name) + + // TODO(scaffolding): add all of the fields supported in the SwiftContainerResourceStatus struct + // If a zero-value isn't expected in the response, place it behind a conditional + + if osResource.Description != "" { + resourceStatus.WithDescription(osResource.Description) + } + + statusApply.WithResource(resourceStatus) +} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml new file mode 100644 index 000000000..599081301 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-create-full +status: + resource: + name: swiftcontainer-create-full-override + description: SwiftContainer from "create full" test + # TODO(scaffolding): Add all fields the resource supports + 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: SwiftContainer + name: swiftcontainer-create-full + ref: swiftcontainer +assertAll: + - celExpr: "swiftcontainer.status.id != ''" + # TODO(scaffolding): Add more checks diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml new file mode 100644 index 000000000..1b05c6fb6 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-create-full +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: swiftcontainer-create-full-override + description: SwiftContainer from "create full" test + # TODO(scaffolding): Add all fields the resource supports diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer/tests/swiftcontainer-create-full/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md new file mode 100644 index 000000000..028caa11f --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md @@ -0,0 +1,11 @@ +# Create a SwiftContainer with all the options + +## Step 00 + +Create a SwiftContainer 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/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml new file mode 100644 index 000000000..9d9e123ea --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-create-minimal +status: + resource: + name: swiftcontainer-create-minimal + # TODO(scaffolding): Add all fields the resource supports + 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: SwiftContainer + name: swiftcontainer-create-minimal + ref: swiftcontainer +assertAll: + - celExpr: "swiftcontainer.status.id != ''" + # TODO(scaffolding): Add more checks diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml new file mode 100644 index 000000000..b0759616f --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-create-minimal +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + # TODO(scaffolding): Only add the mandatory fields. It's possible the resource + # doesn't have mandatory fields, in that case, leave it empty. + resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer/tests/swiftcontainer-create-minimal/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-assert.yaml new file mode 100644 index 000000000..ee0d964f8 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer' in secret.metadata.finalizers" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml new file mode 100644 index 000000000..1620791b9 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer/tests/swiftcontainer-create-minimal/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md new file mode 100644 index 000000000..4bf56c0c8 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md @@ -0,0 +1,15 @@ +# Create a SwiftContainer with the minimum options + +## Step 00 + +Create a minimal SwiftContainer, 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/swiftcontainer/tests/swiftcontainer-import-error/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-assert.yaml new file mode 100644 index 000000000..b83c5c6b1 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import-error-external-1 +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: SwiftContainer +metadata: + name: swiftcontainer-import-error-external-2 +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/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml new file mode 100644 index 000000000..58c498267 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import-error-external-1 +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: SwiftContainer from "import error" test + # TODO(scaffolding): add any required field +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import-error-external-2 +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: SwiftContainer from "import error" test + # TODO(scaffolding): add any required field diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer/tests/swiftcontainer-import-error/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-assert.yaml new file mode 100644 index 000000000..1286cd9a7 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-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/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml new file mode 100644 index 000000000..8f46a38c3 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import-error +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + description: SwiftContainer from "import error" test diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md new file mode 100644 index 000000000..afcc914cb --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md @@ -0,0 +1,13 @@ +# Import SwiftContainer with more than one matching resources + +## Step 00 + +Create two SwiftContainers with identical specs. + +## Step 01 + +Ensure that an imported SwiftContainer with a filter matching the resources returns an error. + +## Reference + +https://k-orc.cloud/development/writing-tests/#import-error diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-assert.yaml new file mode 100644 index 000000000..bc1cc936a --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-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/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml new file mode 100644 index 000000000..417dc2777 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + name: swiftcontainer-import-external + description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test + # TODO(scaffolding): Add all fields supported by the filter diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml new file mode 100644 index 000000000..dff8b95c2 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import-external-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 + resource: + name: swiftcontainer-import-external-not-this-one + description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test + # TODO(scaffolding): Add fields necessary to match filter +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-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/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml new file mode 100644 index 000000000..a18860e22 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml @@ -0,0 +1,17 @@ +--- +# This `swiftcontainer-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: SwiftContainer +metadata: + name: swiftcontainer-import-external-not-this-one +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test + # TODO(scaffolding): Add fields necessary to match filter diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml new file mode 100644 index 000000000..9c21f7cc1 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: SwiftContainer + name: swiftcontainer-import-external + ref: swiftcontainer1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: SwiftContainer + name: swiftcontainer-import-external-not-this-one + ref: swiftcontainer2 +assertAll: + - celExpr: "swiftcontainer1.status.id != swiftcontainer2.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import +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 + resource: + name: swiftcontainer-import-external + description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test + # TODO(scaffolding): Add all fields the resource supports diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml new file mode 100644 index 000000000..4728863b6 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-import-external +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test + # TODO(scaffolding): Add fields necessary to match filter diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md new file mode 100644 index 000000000..fcaa0418f --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md @@ -0,0 +1,18 @@ +# Import SwiftContainer + +## Step 00 + +Import a swiftcontainer that matches all fields in the filter, and verify it is waiting for the external resource to be created. + +## Step 01 + +Create a swiftcontainer 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 swiftcontainer matching the filter and verify that the observed status on the imported swiftcontainer corresponds to the spec of the created swiftcontainer. +Also, confirm that it does not adopt any swiftcontainer whose name is a superstring of its own. + +## Reference + +https://k-orc.cloud/development/writing-tests/#import diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml new file mode 100644 index 000000000..6a3006cba --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: SwiftContainer + name: swiftcontainer-update + ref: swiftcontainer +assertAll: + - celExpr: "!has(swiftcontainer.status.resource.description)" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-update +status: + resource: + name: swiftcontainer-update + # TODO(scaffolding): Add matches for more fields + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml new file mode 100644 index 000000000..2475e6dc9 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-update +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created or updated + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + # TODO(scaffolding): Only add the mandatory fields. It's possible the resource + # doesn't have mandatory fields, in that case, leave it empty. + resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/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/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml new file mode 100644 index 000000000..21c2efecb --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-update +status: + resource: + name: swiftcontainer-update-updated + description: swiftcontainer-update-updated + # TODO(scaffolding): match all fields that were modified + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml new file mode 100644 index 000000000..5e827959b --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-update +spec: + resource: + name: swiftcontainer-update-updated + description: swiftcontainer-update-updated + # TODO(scaffolding): update all mutable fields diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml new file mode 100644 index 000000000..55adf7b7f --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: SwiftContainer + name: swiftcontainer-update + ref: swiftcontainer +assertAll: + - celExpr: "!has(swiftcontainer.status.resource.description)" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-update +status: + resource: + name: swiftcontainer-update + # TODO(scaffolding): validate that updated fields were all reverted to their original value + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml new file mode 100644 index 000000000..2c6c253ff --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-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/swiftcontainer/tests/swiftcontainer-update/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md new file mode 100644 index 000000000..db4e8ac12 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md @@ -0,0 +1,17 @@ +# Update SwiftContainer + +## Step 00 + +Create a SwiftContainer 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/osclients/swiftcontainer.go b/internal/osclients/swiftcontainer.go new file mode 100644 index 000000000..9fffd783c --- /dev/null +++ b/internal/osclients/swiftcontainer.go @@ -0,0 +1,104 @@ +/* +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 osclients + +import ( + "context" + "fmt" + "iter" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" + "github.com/gophercloud/utils/v2/openstack/clientconfig" +) + +type SwiftContainerClient interface { + ListSwiftContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] + CreateSwiftContainer(ctx context.Context, opts containers.CreateOptsBuilder) (*containers.Container, error) + DeleteSwiftContainer(ctx context.Context, resourceID string) error + GetSwiftContainer(ctx context.Context, resourceID string) (*containers.Container, error) + UpdateSwiftContainer(ctx context.Context, id string, opts containers.UpdateOptsBuilder) (*containers.Container, error) +} + +type swiftcontainerClient struct{ client *gophercloud.ServiceClient } + +// NewSwiftContainerClient returns a new OpenStack client. +func NewSwiftContainerClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (SwiftContainerClient, error) { + client, err := openstack.NewObjectStorageV1(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + + if err != nil { + return nil, fmt.Errorf("failed to create swiftcontainer service client: %v", err) + } + + return &swiftcontainerClient{client}, nil +} + +func (c swiftcontainerClient) ListSwiftContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { + pager := containers.List(c.client, listOpts) + return func(yield func(*containers.Container, error) bool) { + _ = pager.EachPage(ctx, yieldPage(containers.ExtractContainers, yield)) + } +} + +func (c swiftcontainerClient) CreateSwiftContainer(ctx context.Context, opts containers.CreateOptsBuilder) (*containers.Container, error) { + return containers.Create(ctx, c.client, opts).Extract() +} + +func (c swiftcontainerClient) DeleteSwiftContainer(ctx context.Context, resourceID string) error { + return containers.Delete(ctx, c.client, resourceID).ExtractErr() +} + +func (c swiftcontainerClient) GetSwiftContainer(ctx context.Context, resourceID string) (*containers.Container, error) { + return containers.Get(ctx, c.client, resourceID).Extract() +} + +func (c swiftcontainerClient) UpdateSwiftContainer(ctx context.Context, id string, opts containers.UpdateOptsBuilder) (*containers.Container, error) { + return containers.Update(ctx, c.client, id, opts).Extract() +} + +type swiftcontainerErrorClient struct{ error } + +// NewSwiftContainerErrorClient returns a SwiftContainerClient in which every method returns the given error. +func NewSwiftContainerErrorClient(e error) SwiftContainerClient { + return swiftcontainerErrorClient{e} +} + +func (e swiftcontainerErrorClient) ListSwiftContainers(_ context.Context, _ containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { + return func(yield func(*containers.Container, error) bool) { + yield(nil, e.error) + } +} + +func (e swiftcontainerErrorClient) CreateSwiftContainer(_ context.Context, _ containers.CreateOptsBuilder) (*containers.Container, error) { + return nil, e.error +} + +func (e swiftcontainerErrorClient) DeleteSwiftContainer(_ context.Context, _ string) error { + return e.error +} + +func (e swiftcontainerErrorClient) GetSwiftContainer(_ context.Context, _ string) (*containers.Container, error) { + return nil, e.error +} + +func (e swiftcontainerErrorClient) UpdateSwiftContainer(_ context.Context, _ string, _ containers.UpdateOptsBuilder) (*containers.Container, error) { + return nil, e.error +} diff --git a/test/apivalidations/swiftcontainer_test.go b/test/apivalidations/swiftcontainer_test.go new file mode 100644 index 000000000..5e3df77b5 --- /dev/null +++ b/test/apivalidations/swiftcontainer_test.go @@ -0,0 +1,105 @@ +/* +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 apivalidations + +import ( + . "github.com/onsi/ginkgo/v2" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + applyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" +) + +const ( + swiftcontainerName = "swiftcontainer" + swiftcontainerID = "265c9e4f-0f5a-46e4-9f3f-fb8de25ae120" +) + +func swiftcontainerStub(namespace *corev1.Namespace) *orcv1alpha1.SwiftContainer { + obj := &orcv1alpha1.SwiftContainer{} + obj.Name = swiftcontainerName + obj.Namespace = namespace.Name + return obj +} + +func testSwiftContainerResource() *applyconfigv1alpha1.SwiftContainerResourceSpecApplyConfiguration { + return applyconfigv1alpha1.SwiftContainerResourceSpec() +} + +func baseSwiftContainerPatch(obj client.Object) *applyconfigv1alpha1.SwiftContainerApplyConfiguration { + return applyconfigv1alpha1.SwiftContainer(obj.GetName(), obj.GetNamespace()). + WithSpec(applyconfigv1alpha1.SwiftContainerSpec(). + WithCloudCredentialsRef(testCredentials())) +} + +func testSwiftContainerImport() *applyconfigv1alpha1.SwiftContainerImportApplyConfiguration { + return applyconfigv1alpha1.SwiftContainerImport().WithID(swiftcontainerID) +} + +var _ = Describe("ORC SwiftContainer API validations", func() { + var namespace *corev1.Namespace + BeforeEach(func() { + namespace = createNamespace() + }) + + runManagementPolicyTests(func() *corev1.Namespace { return namespace }, managementPolicyTestArgs[*applyconfigv1alpha1.SwiftContainerApplyConfiguration]{ + createObject: func(ns *corev1.Namespace) client.Object { return swiftcontainerStub(ns) }, + basePatch: func(obj client.Object) *applyconfigv1alpha1.SwiftContainerApplyConfiguration { + return baseSwiftContainerPatch(obj) + }, + applyResource: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithResource(testSwiftContainerResource()) + }, + applyImport: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithImport(testSwiftContainerImport()) + }, + applyEmptyImport: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.SwiftContainerImport()) + }, + applyEmptyFilter: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.SwiftContainerImport().WithFilter(applyconfigv1alpha1.SwiftContainerFilter())) + }, + applyValidFilter: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.SwiftContainerImport().WithFilter(applyconfigv1alpha1.SwiftContainerFilter().WithName("foo"))) + }, + applyManaged: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged) + }, + applyUnmanaged: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + }, + applyManagedOptions: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { + p.Spec.WithManagedOptions(applyconfigv1alpha1.ManagedOptions().WithOnDelete(orcv1alpha1.OnDeleteDetach)) + }, + getManagementPolicy: func(obj client.Object) orcv1alpha1.ManagementPolicy { + return obj.(*orcv1alpha1.SwiftContainer).Spec.ManagementPolicy + }, + getOnDelete: func(obj client.Object) orcv1alpha1.OnDelete { + return obj.(*orcv1alpha1.SwiftContainer).Spec.ManagedOptions.OnDelete + }, + }) + + // TODO(scaffolding): Add more resource-specific validation tests. + // Some common things to test: + // - Immutability of fields with `self == oldSelf` validation + // - Enum validation (valid and invalid values) + // - Numeric range validation (min/max bounds) + // - Tag uniqueness (if the resource has tags with listType=set) + // - Format validation (CIDR, UUID, etc.) + // - Cross-field validation rules +}) diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index de769a2d0..3af0f3c51 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -2596,6 +2596,8 @@ _Appears in:_ - [ShareNetworkResourceSpec](#sharenetworkresourcespec) - [SubnetFilter](#subnetfilter) - [SubnetResourceSpec](#subnetresourcespec) +- [SwiftContainerFilter](#swiftcontainerfilter) +- [SwiftContainerResourceSpec](#swiftcontainerresourcespec) - [TrunkFilter](#trunkfilter) - [TrunkResourceSpec](#trunkresourcespec) - [UserFilter](#userfilter) @@ -4601,6 +4603,12 @@ _Appears in:_ | `resource` _[SubnetResourceStatus](#subnetresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| + + + + + + #### Trunk From 7ccafd3b0767eb1251b68a813eacfa478cecb880 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Tue, 23 Jun 2026 10:30:39 +0300 Subject: [PATCH 2/7] SwiftContainer: implement controller Add the SwiftContainer API, generated clients, OpenStack client wrapper, controller wiring, status writer, and reconciliation logic. Swift containers are name-addressed, so the controller uses the container name as the ORC status ID and adds custom import handling for name-based imports. --- .gitignore | 4 + PROJECT | 8 + api/v1alpha1/swiftcontainer_types.go | 172 +++++-- api/v1alpha1/zz_generated.deepcopy.go | 219 ++++++++- .../zz_generated.swiftcontainer-resource.go | 158 +++++++ cmd/manager/main.go | 2 + cmd/models-schema/zz_generated.openapi.go | 418 +++++++++++++++++- cmd/resource-generator/data/adapter.template | 8 + cmd/resource-generator/data/api.template | 4 +- cmd/resource-generator/main.go | 8 + ...openstack.k-orc.cloud_swiftcontainers.yaml | 410 +++++++++++++++++ config/crd/kustomization.yaml | 1 + .../bases/orc.clusterserviceversion.yaml | 5 + config/samples/kustomization.yaml | 1 + .../openstack_v1alpha1_swiftcontainer.yaml | 4 +- .../controllers/swiftcontainer/actuator.go | 359 +++++++++++---- .../controllers/swiftcontainer/controller.go | 2 +- internal/controllers/swiftcontainer/status.go | 57 ++- .../swiftcontainer/zz_generated.adapter.go | 92 ++++ .../swiftcontainer/zz_generated.controller.go | 45 ++ internal/osclients/mock/doc.go | 3 + internal/osclients/mock/swiftcontainer.go | 146 ++++++ internal/osclients/swiftcontainer.go | 69 +-- internal/scope/mock.go | 7 + internal/scope/provider.go | 4 + internal/scope/scope.go | 1 + .../api/v1alpha1/swiftcontainer.go | 281 ++++++++++++ .../api/v1alpha1/swiftcontainerfilter.go | 52 +++ .../api/v1alpha1/swiftcontainerimport.go | 52 +++ .../api/v1alpha1/swiftcontainermetadata.go | 48 ++ .../v1alpha1/swiftcontainermetadatastatus.go | 48 ++ .../v1alpha1/swiftcontainerresourcespec.go | 84 ++++ .../v1alpha1/swiftcontainerresourcestatus.go | 125 ++++++ .../api/v1alpha1/swiftcontainerspec.go | 79 ++++ .../api/v1alpha1/swiftcontainerstatus.go | 66 +++ .../applyconfiguration/internal/internal.go | 151 +++++++ pkg/clients/applyconfiguration/utils.go | 18 + .../typed/api/v1alpha1/api_client.go | 5 + .../api/v1alpha1/fake/fake_api_client.go | 4 + .../api/v1alpha1/fake/fake_swiftcontainer.go | 53 +++ .../typed/api/v1alpha1/generated_expansion.go | 2 + .../typed/api/v1alpha1/swiftcontainer.go | 74 ++++ .../api/v1alpha1/interface.go | 7 + .../api/v1alpha1/swiftcontainer.go | 102 +++++ .../informers/externalversions/generic.go | 2 + .../api/v1alpha1/expansion_generated.go | 8 + .../listers/api/v1alpha1/swiftcontainer.go | 70 +++ test/apivalidations/swiftcontainer_test.go | 105 ----- 48 files changed, 3355 insertions(+), 288 deletions(-) create mode 100644 api/v1alpha1/zz_generated.swiftcontainer-resource.go create mode 100644 config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml create mode 100644 internal/controllers/swiftcontainer/zz_generated.adapter.go create mode 100644 internal/controllers/swiftcontainer/zz_generated.controller.go create mode 100644 internal/osclients/mock/swiftcontainer.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainer.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerfilter.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerimport.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcespec.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerspec.go create mode 100644 pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerstatus.go create mode 100644 pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_swiftcontainer.go create mode 100644 pkg/clients/clientset/clientset/typed/api/v1alpha1/swiftcontainer.go create mode 100644 pkg/clients/informers/externalversions/api/v1alpha1/swiftcontainer.go create mode 100644 pkg/clients/listers/api/v1alpha1/swiftcontainer.go delete mode 100644 test/apivalidations/swiftcontainer_test.go diff --git a/.gitignore b/.gitignore index e859277d7..545415b62 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ bin/* Dockerfile.cross +# Root-level compiled binaries +/manager +/resource-generator + # Test binary, built with `go test -c` *.test diff --git a/PROJECT b/PROJECT index e5a188a8b..d02355f97 100644 --- a/PROJECT +++ b/PROJECT @@ -176,6 +176,14 @@ resources: kind: Subnet path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: k-orc.cloud + group: openstack + kind: SwiftContainer + path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/swiftcontainer_types.go b/api/v1alpha1/swiftcontainer_types.go index 5be3eb199..a69193873 100644 --- a/api/v1alpha1/swiftcontainer_types.go +++ b/api/v1alpha1/swiftcontainer_types.go @@ -16,26 +16,42 @@ limitations under the License. package v1alpha1 -// SwiftContainerResourceSpec contains the desired state of the resource. -type SwiftContainerResourceSpec 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"` +// SwiftContainerName is the name of a Swift container. It must be between 1 +// and 256 characters long and must not contain forward slashes. +// +kubebuilder:validation:MinLength:=1 +// +kubebuilder:validation:MaxLength:=256 +// +kubebuilder:validation:Pattern:=`^[^/]+$` +// +kubebuilder:validation:XValidation:rule="self.size() <= 256",message="name must not exceed 256 UTF-8 bytes" +type SwiftContainerName string - // description is a human-readable description for the resource. +// SwiftContainerMetadata defines a key-value pair to be set as a Swift +// container metadata header (X-Container-Meta-: ). +type SwiftContainerMetadata struct { + // key is the name of the metadata item. It will be used as the suffix of + // the X-Container-Meta-* header. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=255 + // +required + Key string `json:"key,omitempty"` + + // value is the value of the metadata item. + // +kubebuilder:validation:MaxLength:=255 + // +required + Value string `json:"value"` +} + +// SwiftContainerMetadataStatus represents an observed metadata key-value pair +// on a Swift container. +type SwiftContainerMetadataStatus struct { + // key is the name of the metadata item. + // +kubebuilder:validation:MaxLength:=255 // +optional - Description *string `json:"description,omitempty"` + Key string `json:"key,omitempty"` - // TODO(scaffolding): Add more types. - // To see what is supported, you can take inspiration from the CreateOpts structure from - // github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers - // - // Until you have implemented mutability for the field, you must add a CEL validation - // preventing the field being modified: - // `// +kubebuilder:validation:XValidation:rule="self == oldSelf",message=" is immutable"` + // value is the value of the metadata item. + // +kubebuilder:validation:MaxLength:=255 + // +optional + Value string `json:"value,omitempty"` } // SwiftContainerFilter defines an existing resource by its properties @@ -43,32 +59,128 @@ type SwiftContainerResourceSpec struct { type SwiftContainerFilter struct { // name of the existing resource // +optional - Name *OpenStackName `json:"name,omitempty"` + Name *SwiftContainerName `json:"name,omitempty"` - // description of the existing resource + // prefix filters containers by name prefix. Only containers whose names + // begin with this prefix will be considered. // +kubebuilder:validation:MinLength:=1 - // +kubebuilder:validation:MaxLength:=255 + // +kubebuilder:validation:MaxLength:=256 + // +optional + Prefix *string `json:"prefix,omitempty"` +} + +// SwiftContainerImport specifies an existing resource which will be imported +// instead of creating a new one. Swift containers are identified by name +// rather than UUID. +// +kubebuilder:validation:MinProperties:=1 +// +kubebuilder:validation:MaxProperties:=1 +type SwiftContainerImport struct { + // name contains the name of an existing Swift container to import. Note + // that when specifying an import by name, the resource MUST already exist. + // The ORC object will enter an error state if the resource does not exist. + // +optional + Name *SwiftContainerName `json:"name,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 *SwiftContainerFilter `json:"filter,omitempty"` +} + +// SwiftContainerResourceSpec contains the desired state of a Swift container. +type SwiftContainerResourceSpec struct { + // name will be the name of the created Swift container. If not specified, + // the name of the ORC object will be used. The name must be unique within + // the account and must not contain forward slashes. + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" + Name *SwiftContainerName `json:"name,omitempty"` + + // metadata is a list of key-value pairs which will be set as + // X-Container-Meta-* headers on the Swift container. + // +kubebuilder:validation:MaxItems:=64 + // +listType=atomic + // +optional + Metadata []SwiftContainerMetadata `json:"metadata,omitempty"` + + // containerRead sets the X-Container-Read ACL header which defines who + // can read objects in the container. Common values include ".r:*" for + // public read access or a comma-separated list of account/container + // combinations. + // +kubebuilder:validation:MaxLength:=256 // +optional - Description *string `json:"description,omitempty"` + ContainerRead *string `json:"containerRead,omitempty"` - // TODO(scaffolding): Add more types. - // To see what is supported, you can take inspiration from the ListOpts structure from - // github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers + // containerWrite sets the X-Container-Write ACL header which defines who + // can write objects to the container. Common values include a + // comma-separated list of account/container combinations. + // +kubebuilder:validation:MaxLength:=256 + // +optional + ContainerWrite *string `json:"containerWrite,omitempty"` + + // storagePolicy is the name of the storage policy to use for this + // container. If not specified, the cluster's default storage policy will + // be used. This field is immutable after creation. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="storagePolicy is immutable" + StoragePolicy *string `json:"storagePolicy,omitempty"` } -// SwiftContainerResourceStatus represents the observed state of the resource. +// SwiftContainerResourceStatus represents the observed state of a Swift container. type SwiftContainerResourceStatus struct { - // name is a Human-readable name for the resource. Might not be unique. - // +kubebuilder:validation:MaxLength=1024 + // name is the name of the Swift container. + // +kubebuilder:validation:MaxLength=256 // +optional Name string `json:"name,omitempty"` - // description is a human-readable description for the resource. + // bytesUsed is the total number of bytes stored in the container. + // +optional + BytesUsed int64 `json:"bytesUsed,omitempty"` + + // objectCount is the number of objects stored in the container. + // +optional + ObjectCount int64 `json:"objectCount,omitempty"` + + // metadata is the list of observed metadata key-value pairs on the container. + // +kubebuilder:validation:MaxItems:=64 + // +listType=atomic + // +optional + Metadata []SwiftContainerMetadataStatus `json:"metadata,omitempty"` + + // containerRead is the current X-Container-Read ACL, defining who can + // read objects in the container. // +kubebuilder:validation:MaxLength=1024 // +optional - Description string `json:"description,omitempty"` + ContainerRead string `json:"containerRead,omitempty"` - // TODO(scaffolding): Add more types. - // To see what is supported, you can take inspiration from the Container structure from - // github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers + // containerWrite is the current X-Container-Write ACL, defining who can + // write objects to the container. + // +kubebuilder:validation:MaxLength=1024 + // +optional + ContainerWrite string `json:"containerWrite,omitempty"` + + // storagePolicy is the name of the storage policy assigned to the container. + // +kubebuilder:validation:MaxLength=1024 + // +optional + StoragePolicy string `json:"storagePolicy,omitempty"` + + // versions is the container where object versions are stored, if versioning + // is enabled on this container. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Versions string `json:"versions,omitempty"` + + // quotaBytes is the quota on the maximum number of bytes that can be + // stored in the container, if set. + // +optional + QuotaBytes *int64 `json:"quotaBytes,omitempty"` + + // quotaCount is the quota on the maximum number of objects that can be + // stored in the container, if set. + // +optional + QuotaCount *int64 `json:"quotaCount,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d802d007e..1fbc5462d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6183,16 +6183,43 @@ func (in *SubnetStatus) DeepCopy() *SubnetStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainer) DeepCopyInto(out *SwiftContainer) { + *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 SwiftContainer. +func (in *SwiftContainer) DeepCopy() *SwiftContainer { + if in == nil { + return nil + } + out := new(SwiftContainer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SwiftContainer) 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 *SwiftContainerFilter) DeepCopyInto(out *SwiftContainerFilter) { *out = *in if in.Name != nil { in, out := &in.Name, &out.Name - *out = new(OpenStackName) + *out = new(SwiftContainerName) **out = **in } - if in.Description != nil { - in, out := &in.Description, &out.Description + if in.Prefix != nil { + in, out := &in.Prefix, &out.Prefix *out = new(string) **out = **in } @@ -6208,16 +6235,118 @@ func (in *SwiftContainerFilter) DeepCopy() *SwiftContainerFilter { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerImport) DeepCopyInto(out *SwiftContainerImport) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(SwiftContainerName) + **out = **in + } + if in.Filter != nil { + in, out := &in.Filter, &out.Filter + *out = new(SwiftContainerFilter) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerImport. +func (in *SwiftContainerImport) DeepCopy() *SwiftContainerImport { + if in == nil { + return nil + } + out := new(SwiftContainerImport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerList) DeepCopyInto(out *SwiftContainerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SwiftContainer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerList. +func (in *SwiftContainerList) DeepCopy() *SwiftContainerList { + if in == nil { + return nil + } + out := new(SwiftContainerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SwiftContainerList) 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 *SwiftContainerMetadata) DeepCopyInto(out *SwiftContainerMetadata) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerMetadata. +func (in *SwiftContainerMetadata) DeepCopy() *SwiftContainerMetadata { + if in == nil { + return nil + } + out := new(SwiftContainerMetadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerMetadataStatus) DeepCopyInto(out *SwiftContainerMetadataStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerMetadataStatus. +func (in *SwiftContainerMetadataStatus) DeepCopy() *SwiftContainerMetadataStatus { + if in == nil { + return nil + } + out := new(SwiftContainerMetadataStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SwiftContainerResourceSpec) DeepCopyInto(out *SwiftContainerResourceSpec) { *out = *in if in.Name != nil { in, out := &in.Name, &out.Name - *out = new(OpenStackName) + *out = new(SwiftContainerName) **out = **in } - if in.Description != nil { - in, out := &in.Description, &out.Description + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make([]SwiftContainerMetadata, len(*in)) + copy(*out, *in) + } + if in.ContainerRead != nil { + in, out := &in.ContainerRead, &out.ContainerRead + *out = new(string) + **out = **in + } + if in.ContainerWrite != nil { + in, out := &in.ContainerWrite, &out.ContainerWrite + *out = new(string) + **out = **in + } + if in.StoragePolicy != nil { + in, out := &in.StoragePolicy, &out.StoragePolicy *out = new(string) **out = **in } @@ -6236,6 +6365,21 @@ func (in *SwiftContainerResourceSpec) DeepCopy() *SwiftContainerResourceSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SwiftContainerResourceStatus) DeepCopyInto(out *SwiftContainerResourceStatus) { *out = *in + if in.Metadata != nil { + in, out := &in.Metadata, &out.Metadata + *out = make([]SwiftContainerMetadataStatus, len(*in)) + copy(*out, *in) + } + if in.QuotaBytes != nil { + in, out := &in.QuotaBytes, &out.QuotaBytes + *out = new(int64) + **out = **in + } + if in.QuotaCount != nil { + in, out := &in.QuotaCount, &out.QuotaCount + *out = new(int64) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerResourceStatus. @@ -6248,6 +6392,69 @@ func (in *SwiftContainerResourceStatus) DeepCopy() *SwiftContainerResourceStatus return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerSpec) DeepCopyInto(out *SwiftContainerSpec) { + *out = *in + if in.Import != nil { + in, out := &in.Import, &out.Import + *out = new(SwiftContainerImport) + (*in).DeepCopyInto(*out) + } + if in.Resource != nil { + in, out := &in.Resource, &out.Resource + *out = new(SwiftContainerResourceSpec) + (*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 SwiftContainerSpec. +func (in *SwiftContainerSpec) DeepCopy() *SwiftContainerSpec { + if in == nil { + return nil + } + out := new(SwiftContainerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SwiftContainerStatus) DeepCopyInto(out *SwiftContainerStatus) { + *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(SwiftContainerResourceStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerStatus. +func (in *SwiftContainerStatus) DeepCopy() *SwiftContainerStatus { + if in == nil { + return nil + } + out := new(SwiftContainerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Trunk) DeepCopyInto(out *Trunk) { *out = *in diff --git a/api/v1alpha1/zz_generated.swiftcontainer-resource.go b/api/v1alpha1/zz_generated.swiftcontainer-resource.go new file mode 100644 index 000000000..87787b96e --- /dev/null +++ b/api/v1alpha1/zz_generated.swiftcontainer-resource.go @@ -0,0 +1,158 @@ +// 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" +) + +// SwiftContainerSpec 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 SwiftContainerSpec struct { + // import refers to an existing OpenStack resource which will be imported instead of + // creating a new one. + // +optional + Import *SwiftContainerImport `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 *SwiftContainerResourceSpec `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,omitzero"` +} + +// SwiftContainerStatus defines the observed state of an ORC resource. +type SwiftContainerStatus 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. + // +kubebuilder:validation:MaxLength:=1024 + // +optional + ID *string `json:"id,omitempty"` + + // resource contains the observed state of the OpenStack resource. + // +optional + Resource *SwiftContainerResourceStatus `json:"resource,omitempty"` +} + +var _ ObjectWithConditions = &SwiftContainer{} + +func (i *SwiftContainer) 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" + +// SwiftContainer is the Schema for an ORC resource. +type SwiftContainer struct { + metav1.TypeMeta `json:",inline"` + + // metadata contains the object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec specifies the desired state of the resource. + // +required + Spec SwiftContainerSpec `json:"spec,omitzero"` + + // status defines the observed state of the resource. + // +optional + Status SwiftContainerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// SwiftContainerList contains a list of SwiftContainer. +type SwiftContainerList struct { + metav1.TypeMeta `json:",inline"` + + // metadata contains the list metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items contains a list of SwiftContainer. + // +required + Items []SwiftContainer `json:"items"` +} + +func (l *SwiftContainerList) GetItems() []SwiftContainer { + return l.Items +} + +func init() { + SchemeBuilder.Register(&SwiftContainer{}, &SwiftContainerList{}) +} + +func (i *SwiftContainer) GetCloudCredentialsRef() (*string, *CloudCredentialsReference) { + if i == nil { + return nil, nil + } + + return &i.Namespace, &i.Spec.CloudCredentialsRef +} + +var _ CloudCredentialsRefProvider = &SwiftContainer{} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9addc552d..3ffd04e41 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -49,6 +49,7 @@ import ( "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/service" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/sharenetwork" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/subnet" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/swiftcontainer" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/trunk" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/user" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/volume" @@ -138,6 +139,7 @@ func main() { keypair.New(scopeFactory), group.New(scopeFactory), role.New(scopeFactory), + swiftcontainer.New(scopeFactory), } restConfig := ctrl.GetConfigOrDie() diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 0a5bb558b..76091a33b 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -240,9 +240,16 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SubnetResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SubnetResourceStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SubnetSpec": schema_openstack_resource_controller_v2_api_v1alpha1_SubnetSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SubnetStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SubnetStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainer": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainer(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerFilter": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerFilter(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerImport": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerImport(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerList": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerList(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerMetadata": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerMetadataStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadataStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResourceSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResourceStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerSpec": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerStatus": schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Trunk": schema_openstack_resource_controller_v2_api_v1alpha1_Trunk(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.TrunkFilter": schema_openstack_resource_controller_v2_api_v1alpha1_TrunkFilter(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.TrunkImport": schema_openstack_resource_controller_v2_api_v1alpha1_TrunkImport(ref), @@ -11746,6 +11753,57 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SubnetStatus(ref commo } } +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainer(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainer 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.SwiftContainerSpec"), + }, + }, + "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.SwiftContainerStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -11760,9 +11818,144 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerFilter(r Format: "", }, }, - "description": { + "prefix": { SchemaProps: spec.SchemaProps{ - Description: "description of the existing resource", + Description: "prefix filters containers by name prefix. Only containers whose names begin with this prefix will be considered.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerImport(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerImport specifies an existing resource which will be imported instead of creating a new one. Swift containers are identified by name rather than UUID.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name contains the name of an existing Swift container to import. Note that when specifying an import by name, 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.SwiftContainerFilter"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerFilter"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerList contains a list of SwiftContainer.", + 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 SwiftContainer.", + 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.SwiftContainer"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainer", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerMetadata defines a key-value pair to be set as a Swift container metadata header (X-Container-Meta-: ).", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the name of the metadata item. It will be used as the suffix of the X-Container-Meta-* header.", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "value is the value of the metadata item.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"key", "value"}, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadataStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerMetadataStatus represents an observed metadata key-value pair on a Swift container.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the name of the metadata item.", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Description: "value is the value of the metadata item.", Type: []string{"string"}, Format: "", }, @@ -11777,19 +11970,52 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "SwiftContainerResourceSpec contains the desired state of the resource.", + Description: "SwiftContainerResourceSpec contains the desired state of a Swift container.", 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.", + Description: "name will be the name of the created Swift container. If not specified, the name of the ORC object will be used. The name must be unique within the account and must not contain forward slashes.", Type: []string{"string"}, Format: "", }, }, - "description": { + "metadata": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, SchemaProps: spec.SchemaProps{ - Description: "description is a human-readable description for the resource.", + Description: "metadata is a list of key-value pairs which will be set as X-Container-Meta-* headers on the Swift container.", + 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.SwiftContainerMetadata"), + }, + }, + }, + }, + }, + "containerRead": { + SchemaProps: spec.SchemaProps{ + Description: "containerRead sets the X-Container-Read ACL header which defines who can read objects in the container. Common values include \".r:*\" for public read access or a comma-separated list of account/container combinations.", + Type: []string{"string"}, + Format: "", + }, + }, + "containerWrite": { + SchemaProps: spec.SchemaProps{ + Description: "containerWrite sets the X-Container-Write ACL header which defines who can write objects to the container. Common values include a comma-separated list of account/container combinations.", + Type: []string{"string"}, + Format: "", + }, + }, + "storagePolicy": { + SchemaProps: spec.SchemaProps{ + Description: "storagePolicy is the name of the storage policy to use for this container. If not specified, the cluster's default storage policy will be used. This field is immutable after creation.", Type: []string{"string"}, Format: "", }, @@ -11797,6 +12023,8 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource }, }, }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerMetadata"}, } } @@ -11804,26 +12032,196 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "SwiftContainerResourceStatus represents the observed state of the resource.", + Description: "SwiftContainerResourceStatus represents the observed state of a Swift container.", 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.", + Description: "name is the name of the Swift container.", Type: []string{"string"}, Format: "", }, }, - "description": { + "bytesUsed": { SchemaProps: spec.SchemaProps{ - Description: "description is a human-readable description for the resource.", + Description: "bytesUsed is the total number of bytes stored in the container.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "objectCount": { + SchemaProps: spec.SchemaProps{ + Description: "objectCount is the number of objects stored in the container.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "metadata": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "metadata is the list of observed metadata key-value pairs on the container.", + 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.SwiftContainerMetadataStatus"), + }, + }, + }, + }, + }, + "containerRead": { + SchemaProps: spec.SchemaProps{ + Description: "containerRead is the current X-Container-Read ACL, defining who can read objects in the container.", + Type: []string{"string"}, + Format: "", + }, + }, + "containerWrite": { + SchemaProps: spec.SchemaProps{ + Description: "containerWrite is the current X-Container-Write ACL, defining who can write objects to the container.", + Type: []string{"string"}, + Format: "", + }, + }, + "storagePolicy": { + SchemaProps: spec.SchemaProps{ + Description: "storagePolicy is the name of the storage policy assigned to the container.", + Type: []string{"string"}, + Format: "", + }, + }, + "versions": { + SchemaProps: spec.SchemaProps{ + Description: "versions is the container where object versions are stored, if versioning is enabled on this container.", + Type: []string{"string"}, + Format: "", + }, + }, + "quotaBytes": { + SchemaProps: spec.SchemaProps{ + Description: "quotaBytes is the quota on the maximum number of bytes that can be stored in the container, if set.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "quotaCount": { + SchemaProps: spec.SchemaProps{ + Description: "quotaCount is the quota on the maximum number of objects that can be stored in the container, if set.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerMetadataStatus"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerSpec 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.SwiftContainerImport"), + }, + }, + "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.SwiftContainerResourceSpec"), + }, + }, + "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.ManagedOptions", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerImport", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerResourceSpec"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SwiftContainerStatus 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.SwiftContainerResourceStatus"), + }, + }, }, }, }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.SwiftContainerResourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, } } diff --git a/cmd/resource-generator/data/adapter.template b/cmd/resource-generator/data/adapter.template index 7bec457ee..62dafde50 100644 --- a/cmd/resource-generator/data/adapter.template +++ b/cmd/resource-generator/data/adapter.template @@ -66,7 +66,15 @@ func (f adapterT) GetImportID() *string { if f.Spec.Import == nil { return nil } +{{- if .HasCustomImport }} + if f.Spec.Import.Name == nil { + return nil + } + name := string(*f.Spec.Import.Name) + return &name +{{- else }} return f.Spec.Import.ID +{{- end }} } func (f adapterT) GetImportFilter() *filterT { diff --git a/cmd/resource-generator/data/api.template b/cmd/resource-generator/data/api.template index 9abb00d7a..0e6435230 100644 --- a/cmd/resource-generator/data/api.template +++ b/cmd/resource-generator/data/api.template @@ -19,7 +19,7 @@ package {{ .APIVersion }} import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) - +{{ if not .HasCustomImport }} // {{ .Name }}Import specifies an existing resource which will be imported instead of // creating a new one // +kubebuilder:validation:MinProperties:=1 @@ -50,7 +50,7 @@ type {{ .Name }}Import struct { // +optional Filter *{{ .Name }}Filter `json:"filter,omitempty"` } - +{{ end }} // {{ .Name }}Spec 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" diff --git a/cmd/resource-generator/main.go b/cmd/resource-generator/main.go index 609bf9084..ffc2bdceb 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -67,6 +67,10 @@ type templateFields struct { // When true, the UUID validation will be omitted from the Import.ID field. // Default is false (uses UUID). UsesNameAsID bool + // HasCustomImport indicates that the resource defines its own Import type in the + // non-generated *_types.go file. When true, the Import type will not be generated. + // Default is false (Import type is generated). + HasCustomImport bool } var resources []templateFields = []templateFields{ @@ -180,6 +184,10 @@ var resources []templateFields = []templateFields{ { Name: "ApplicationCredential", }, + { + Name: "SwiftContainer", + HasCustomImport: true, // SwiftContainerImport is defined in swiftcontainer_types.go + }, } // These resources won't be generated diff --git a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml new file mode 100644 index 000000000..7e8ff758d --- /dev/null +++ b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml @@ -0,0 +1,410 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: swiftcontainers.openstack.k-orc.cloud +spec: + group: openstack.k-orc.cloud + names: + categories: + - openstack + kind: SwiftContainer + listKind: SwiftContainerList + plural: swiftcontainers + singular: swiftcontainer + 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: SwiftContainer 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: + name: + description: name of the existing resource + maxLength: 256 + minLength: 1 + pattern: ^[^/]+$ + type: string + x-kubernetes-validations: + - message: name must not exceed 256 UTF-8 bytes + rule: self.size() <= 256 + prefix: + description: |- + prefix filters containers by name prefix. Only containers whose names + begin with this prefix will be considered. + maxLength: 256 + minLength: 1 + type: string + type: object + name: + description: |- + name contains the name of an existing Swift container to import. Note + that when specifying an import by name, the resource MUST already exist. + The ORC object will enter an error state if the resource does not exist. + maxLength: 256 + minLength: 1 + pattern: ^[^/]+$ + type: string + x-kubernetes-validations: + - message: name must not exceed 256 UTF-8 bytes + rule: self.size() <= 256 + 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: + containerRead: + description: |- + containerRead sets the X-Container-Read ACL header which defines who + can read objects in the container. Common values include ".r:*" for + public read access or a comma-separated list of account/container + combinations. + maxLength: 256 + type: string + containerWrite: + description: |- + containerWrite sets the X-Container-Write ACL header which defines who + can write objects to the container. Common values include a + comma-separated list of account/container combinations. + maxLength: 256 + type: string + metadata: + description: |- + metadata is a list of key-value pairs which will be set as + X-Container-Meta-* headers on the Swift container. + items: + description: |- + SwiftContainerMetadata defines a key-value pair to be set as a Swift + container metadata header (X-Container-Meta-: ). + properties: + key: + description: |- + key is the name of the metadata item. It will be used as the suffix of + the X-Container-Meta-* header. + maxLength: 255 + minLength: 1 + type: string + value: + description: value is the value of the metadata item. + maxLength: 255 + type: string + required: + - key + - value + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + name will be the name of the created Swift container. If not specified, + the name of the ORC object will be used. The name must be unique within + the account and must not contain forward slashes. + maxLength: 256 + minLength: 1 + pattern: ^[^/]+$ + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: name must not exceed 256 UTF-8 bytes + rule: self.size() <= 256 + storagePolicy: + description: |- + storagePolicy is the name of the storage policy to use for this + container. If not specified, the cluster's default storage policy will + be used. This field is immutable after creation. + maxLength: 255 + minLength: 1 + type: string + x-kubernetes-validations: + - message: storagePolicy is immutable + rule: self == oldSelf + type: object + 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. + maxLength: 1024 + type: string + resource: + description: resource contains the observed state of the OpenStack + resource. + properties: + bytesUsed: + description: bytesUsed is the total number of bytes stored in + the container. + format: int64 + type: integer + containerRead: + description: |- + containerRead is the current X-Container-Read ACL, defining who can + read objects in the container. + maxLength: 1024 + type: string + containerWrite: + description: |- + containerWrite is the current X-Container-Write ACL, defining who can + write objects to the container. + maxLength: 1024 + type: string + metadata: + description: metadata is the list of observed metadata key-value + pairs on the container. + items: + description: |- + SwiftContainerMetadataStatus represents an observed metadata key-value pair + on a Swift container. + properties: + key: + description: key is the name of the metadata item. + maxLength: 255 + type: string + value: + description: value is the value of the metadata item. + maxLength: 255 + type: string + type: object + maxItems: 64 + type: array + x-kubernetes-list-type: atomic + name: + description: name is the name of the Swift container. + maxLength: 256 + type: string + objectCount: + description: objectCount is the number of objects stored in the + container. + format: int64 + type: integer + quotaBytes: + description: |- + quotaBytes is the quota on the maximum number of bytes that can be + stored in the container, if set. + format: int64 + type: integer + quotaCount: + description: |- + quotaCount is the quota on the maximum number of objects that can be + stored in the container, if set. + format: int64 + type: integer + storagePolicy: + description: storagePolicy is the name of the storage policy assigned + to the container. + maxLength: 1024 + type: string + versions: + description: |- + versions is the container where object versions are stored, if versioning + is enabled on this container. + maxLength: 1024 + type: string + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 26b47f63f..90a1d3793 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -24,6 +24,7 @@ resources: - bases/openstack.k-orc.cloud_services.yaml - bases/openstack.k-orc.cloud_sharenetworks.yaml - bases/openstack.k-orc.cloud_subnets.yaml +- bases/openstack.k-orc.cloud_swiftcontainers.yaml - bases/openstack.k-orc.cloud_trunks.yaml - bases/openstack.k-orc.cloud_users.yaml - bases/openstack.k-orc.cloud_volumes.yaml diff --git a/config/manifests/bases/orc.clusterserviceversion.yaml b/config/manifests/bases/orc.clusterserviceversion.yaml index 89421fc32..f34c53716 100644 --- a/config/manifests/bases/orc.clusterserviceversion.yaml +++ b/config/manifests/bases/orc.clusterserviceversion.yaml @@ -124,6 +124,11 @@ spec: kind: Subnet name: subnets.openstack.k-orc.cloud version: v1alpha1 + - description: SwiftContainer is the Schema for an ORC resource. + displayName: Swift Container + kind: SwiftContainer + name: swiftcontainers.openstack.k-orc.cloud + version: v1alpha1 - description: Trunk is the Schema for an ORC resource. displayName: Trunk kind: Trunk diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 8a50ba039..94b9199ee 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -22,6 +22,7 @@ resources: - openstack_v1alpha1_service.yaml - openstack_v1alpha1_sharenetwork.yaml - openstack_v1alpha1_subnet.yaml +- openstack_v1alpha1_swiftcontainer.yaml - openstack_v1alpha1_trunk.yaml - openstack_v1alpha1_user.yaml - openstack_v1alpha1_volume.yaml diff --git a/config/samples/openstack_v1alpha1_swiftcontainer.yaml b/config/samples/openstack_v1alpha1_swiftcontainer.yaml index 7ed4f4bf9..94b6fa4a4 100644 --- a/config/samples/openstack_v1alpha1_swiftcontainer.yaml +++ b/config/samples/openstack_v1alpha1_swiftcontainer.yaml @@ -5,10 +5,8 @@ metadata: name: swiftcontainer-sample spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed resource: - description: Sample SwiftContainer - # TODO(scaffolding): Add all fields the resource supports + name: swiftcontainer-sample-name diff --git a/internal/controllers/swiftcontainer/actuator.go b/internal/controllers/swiftcontainer/actuator.go index 91306cb93..08500f004 100644 --- a/internal/controllers/swiftcontainer/actuator.go +++ b/internal/controllers/swiftcontainer/actuator.go @@ -1,5 +1,5 @@ /* -Copyright The ORC Authors. +Copyright 2024 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. @@ -19,84 +19,152 @@ package swiftcontainer import ( "context" "iter" + "strings" "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" 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" + generic "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" + osclients "github.com/k-orc/openstack-resource-controller/v2/internal/osclients" orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" ) +// osContainerT wraps containers.GetHeader with the container name and +// metadata, since GetHeader does not include the container name and the +// X-Container-Meta-* headers are returned separately via ExtractMetadata. +type osContainerT struct { + Name string + Metadata map[string]string + containers.GetHeader +} + // OpenStack resource types type ( - osResourceT = containers.Container + osResourceT = osContainerT - 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] + createResourceActuator = generic.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT] + deleteResourceActuator = generic.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT] + reconcileResourceActuator = generic.ReconcileResourceActuator[orcObjectPT, osResourceT] + resourceReconciler = generic.ResourceReconciler[orcObjectPT, osResourceT] + helperFactory = generic.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT] ) type swiftcontainerActuator struct { - osClient osclients.SwiftContainerClient - k8sClient client.Client + osClient osclients.SwiftContainerClient } var _ createResourceActuator = swiftcontainerActuator{} var _ deleteResourceActuator = swiftcontainerActuator{} +var _ reconcileResourceActuator = swiftcontainerActuator{} -func (swiftcontainerActuator) GetResourceID(osResource *osResourceT) string { - return osResource.ID +func (swiftcontainerActuator) GetResourceID(osResource *osContainerT) string { + // Swift containers are identified by name + return osResource.Name } -func (actuator swiftcontainerActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) { - resource, err := actuator.osClient.GetSwiftContainer(ctx, id) +func (actuator swiftcontainerActuator) GetOSResourceByID(ctx context.Context, id string) (*osContainerT, progress.ReconcileStatus) { + header, err := actuator.osClient.GetContainer(ctx, id, nil) + if err != nil { + return nil, progress.WrapError(err) + } + metadata, err := actuator.osClient.GetContainerMetadata(ctx, id) if err != nil { return nil, progress.WrapError(err) } - return resource, nil + return &osContainerT{Name: id, Metadata: metadata, GetHeader: *header}, nil } -func (actuator swiftcontainerActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) { +func (actuator swiftcontainerActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osContainerT, error], bool) { resourceSpec := orcObject.Spec.Resource if resourceSpec == nil { return nil, false } - // TODO(scaffolding) If you need to filter resources on fields that the List() function - // of gophercloud does not support, it's possible to perform client-side filtering. - // Check osclients.ResourceFilter - - listOpts := containers.ListOpts{ - Name: getResourceName(orcObject), - Description: ptr.Deref(resourceSpec.Description, ""), - } - - return actuator.osClient.ListSwiftContainers(ctx, listOpts), true + name := getResourceName(orcObject) + return func(yield func(*osContainerT, error) bool) { + header, err := actuator.osClient.GetContainer(ctx, name, nil) + if err != nil { + if !orcerrors.IsNotFound(err) { + yield(nil, err) + } + return + } + metadata, err := actuator.osClient.GetContainerMetadata(ctx, name) + if err != nil { + yield(nil, err) + return + } + yield(&osContainerT{Name: name, Metadata: metadata, GetHeader: *header}, nil) + }, true } -func (actuator swiftcontainerActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) { - // TODO(scaffolding) If you need to filter resources on fields that the List() function - // of gophercloud does not support, it's possible to perform client-side filtering. - // Check osclients.ResourceFilter +func (actuator swiftcontainerActuator) ListOSResourcesForImport(ctx context.Context, _ orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) { + return func(yield func(*osContainerT, error) bool) { + if filter.Name != nil { + name := string(*filter.Name) + // If a prefix filter is also set, verify the name satisfies it. + // Without this check a filter {name: "x", prefix: "y-"} would + // silently import "x" even though it does not match the prefix. + if filter.Prefix != nil && !hasPrefix(name, *filter.Prefix) { + return + } + header, err := actuator.osClient.GetContainer(ctx, name, nil) + if err != nil { + if !orcerrors.IsNotFound(err) { + yield(nil, err) + } + return + } + metadata, err := actuator.osClient.GetContainerMetadata(ctx, name) + if err != nil { + yield(nil, err) + return + } + yield(&osContainerT{Name: name, Metadata: metadata, GetHeader: *header}, nil) + } else { + // List all containers and filter by prefix + listOpts := containers.ListOpts{} + for container, err := range actuator.osClient.ListContainers(ctx, listOpts) { + if err != nil { + yield(nil, err) + return + } + + if filter.Prefix != nil && !hasPrefix(container.Name, *filter.Prefix) { + continue + } + + header, err := actuator.osClient.GetContainer(ctx, container.Name, nil) + if err != nil { + yield(nil, err) + return + } + metadata, err := actuator.osClient.GetContainerMetadata(ctx, container.Name) + if err != nil { + yield(nil, err) + return + } + if !yield(&osContainerT{Name: container.Name, Metadata: metadata, GetHeader: *header}, nil) { + return + } + } + } + }, nil +} - listOpts := containers.ListOpts{ - Name: string(ptr.Deref(filter.Name, "")), - Description: string(ptr.Deref(filter.Description, "")), - // TODO(scaffolding): Add more import filters +// hasPrefix checks if name starts with prefix. +func hasPrefix(name, prefix string) bool { + if len(prefix) > len(name) { + return false } - - return actuator.osClient.ListSwiftContainers(ctx, listOpts), nil + return name[:len(prefix)] == prefix } -func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) { +func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osContainerT, progress.ReconcileStatus) { resource := obj.Spec.Resource if resource == nil { @@ -104,13 +172,49 @@ func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj o return nil, progress.WrapError( orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set")) } - createOpts := containers.CreateOpts{ - Name: getResourceName(obj), - Description: ptr.Deref(resource.Description, ""), - // TODO(scaffolding): Add more fields + + name := getResourceName(obj) + + // Swift treats '/' as a path separator in container names, making the name + // invalid at the API level. Validate explicitly to provide a clear error + // message rather than a confusing HTTP error from gophercloud. + if strings.Contains(name, "/") { + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, + "container name must not contain forward slashes")) + } + + // Swift limits container names to 256 UTF-8 bytes. The kubebuilder + // MaxLength:=256 marker counts Unicode code points, not bytes, so a name + // with multi-byte UTF-8 characters can pass API validation yet exceed the + // byte limit. Validate explicitly here to produce a clear error message. + if len(name) > 256 { + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, + "container name must not exceed 256 bytes")) + } + + createOpts := containers.CreateOpts{} + + if resource.ContainerRead != nil { + createOpts.ContainerRead = *resource.ContainerRead + } + if resource.ContainerWrite != nil { + createOpts.ContainerWrite = *resource.ContainerWrite + } + if resource.StoragePolicy != nil { + createOpts.StoragePolicy = *resource.StoragePolicy + } + + if len(resource.Metadata) > 0 { + metadata := make(map[string]string, len(resource.Metadata)) + for _, m := range resource.Metadata { + metadata[m.Key] = m.Value + } + createOpts.Metadata = metadata } - osResource, err := actuator.osClient.CreateSwiftContainer(ctx, createOpts) + _, err := actuator.osClient.CreateContainer(ctx, name, createOpts) if err != nil { if !orcerrors.IsRetryable(err) { err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration creating resource: "+err.Error(), err) @@ -118,44 +222,69 @@ func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj o return nil, progress.WrapError(err) } - return osResource, nil + // Fetch the created container to return its header and metadata + header, err := actuator.osClient.GetContainer(ctx, name, nil) + if err != nil { + return nil, progress.WrapError(err) + } + fetchedMetadata, err := actuator.osClient.GetContainerMetadata(ctx, name) + if err != nil { + return nil, progress.WrapError(err) + } + + return &osContainerT{Name: name, Metadata: fetchedMetadata, GetHeader: *header}, nil +} + +func (actuator swiftcontainerActuator) DeleteResource(ctx context.Context, _ orcObjectPT, osResource *osContainerT) progress.ReconcileStatus { + err := actuator.osClient.DeleteContainer(ctx, osResource.Name) + return progress.WrapError(err) } -func (actuator swiftcontainerActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus { - return progress.WrapError(actuator.osClient.DeleteSwiftContainer(ctx, resource.ID)) +func (actuator swiftcontainerActuator) GetResourceReconcilers(_ context.Context, _ orcObjectPT, _ *osResourceT, _ generic.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { + return []resourceReconciler{ + actuator.reconcileACLs, + actuator.reconcileMetadata, + }, nil } -func (actuator swiftcontainerActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { +// reconcileACLs compares the desired ACLs from the spec with the current +// container ACLs and calls UpdateContainer if they differ. +func (actuator swiftcontainerActuator) reconcileACLs(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { log := ctrl.LoggerFrom(ctx) - resource := obj.Spec.Resource + resource := orcObject.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")) + return nil } - updateOpts := containers.UpdateOpts{} + // GetHeader.Read and GetHeader.Write are []string parsed from the ACL + // headers. We join them back to a comma-separated string for comparison + // with the spec which stores ACLs as a single string. + currentRead := strings.Join(osResource.Read, ",") + currentWrite := strings.Join(osResource.Write, ",") - handleNameUpdate(&updateOpts, obj, osResource) - handleDescriptionUpdate(&updateOpts, resource, osResource) - - // TODO(scaffolding): add handler for all fields supporting mutability - - needsUpdate, err := needsUpdate(updateOpts) - if err != nil { - return progress.WrapError( - orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)) + desiredRead := "" + if resource.ContainerRead != nil { + desiredRead = *resource.ContainerRead } - if !needsUpdate { - log.V(logging.Debug).Info("No changes") - return nil + desiredWrite := "" + if resource.ContainerWrite != nil { + desiredWrite = *resource.ContainerWrite } - _, err = actuator.osClient.UpdateSwiftContainer(ctx, osResource.ID, updateOpts) + if currentRead == desiredRead && currentWrite == desiredWrite { + log.V(logging.Debug).Info("Container ACLs are up to date") + return nil + } + log.V(logging.Info).Info("Updating container ACLs") + updateOpts := containers.UpdateOpts{ + ContainerRead: &desiredRead, + ContainerWrite: &desiredWrite, + } + _, err := actuator.osClient.UpdateContainer(ctx, osResource.Name, updateOpts) if err != nil { if !orcerrors.IsRetryable(err) { - err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err) + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating container ACLs: "+err.Error(), err) } return progress.WrapError(err) } @@ -163,45 +292,88 @@ func (actuator swiftcontainerActuator) updateResource(ctx context.Context, obj o return progress.NeedsRefresh() } -func needsUpdate(updateOpts containers.UpdateOpts) (bool, error) { - updateOptsMap, err := updateOpts.ToContainerUpdateMap() +// reconcileMetadata compares the desired metadata from the spec with the +// current container metadata and calls UpdateContainer if they differ. +func (actuator swiftcontainerActuator) reconcileMetadata(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { + log := ctrl.LoggerFrom(ctx) + resource := orcObject.Spec.Resource + if resource == nil { + return nil + } + + // Fetch current metadata via GetContainerMetadata. Keys are returned in + // canonical HTTP header form (e.g. "Env", not "env") because Go's net/http + // canonicalises header keys on retrieval. + currentMetadata, err := actuator.osClient.GetContainerMetadata(ctx, osResource.Name) if err != nil { - return false, err + return progress.WrapError(err) } - updateMap, ok := updateOptsMap["container"].(map[string]any) - if !ok { - updateMap = make(map[string]any) + // Build a lowercase-keyed view of current metadata for case-insensitive + // comparison. We also track the original canonical key so we can form + // correct X-Remove-Container-Meta- headers when removing entries. + currentLower := make(map[string]string, len(currentMetadata)) // lowercase key -> value + currentCanonical := make(map[string]string, len(currentMetadata)) // lowercase key -> canonical key + for k, v := range currentMetadata { + lk := strings.ToLower(k) + currentLower[lk] = v + currentCanonical[lk] = k } - return len(updateMap) > 0, nil -} + // Build the desired metadata map from the spec with lowercase keys so that + // comparisons are case-insensitive (metadata key casing is not significant + // in Swift). + desiredMetadata := make(map[string]string, len(resource.Metadata)) + for _, m := range resource.Metadata { + desiredMetadata[strings.ToLower(m.Key)] = m.Value + } -func handleNameUpdate(updateOpts *containers.UpdateOpts, obj orcObjectPT, osResource *osResourceT) { - name := getResourceName(obj) - if osResource.Name != name { - updateOpts.Name = &name + // Find keys to add/update and keys to remove. + var toSet map[string]string + var toRemove []string + + for key, desiredVal := range desiredMetadata { + if currentVal, exists := currentLower[key]; !exists || currentVal != desiredVal { + if toSet == nil { + toSet = make(map[string]string) + } + toSet[key] = desiredVal + } + } + for lk := range currentLower { + if _, desired := desiredMetadata[lk]; !desired { + // Use the canonical key so that the X-Remove-Container-Meta- + // header matches what Swift has stored. + toRemove = append(toRemove, currentCanonical[lk]) + } } -} -func handleDescriptionUpdate(updateOpts *containers.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { - description := ptr.Deref(resource.Description, "") - if osResource.Description != description { - updateOpts.Description = &description + if len(toSet) == 0 && len(toRemove) == 0 { + log.V(logging.Debug).Info("Container metadata is up to date") + return nil } -} -func (actuator swiftcontainerActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { - return []resourceReconciler{ - actuator.updateResource, - }, nil + log.V(logging.Info).Info("Updating container metadata") + updateOpts := containers.UpdateOpts{ + Metadata: toSet, + RemoveMetadata: toRemove, + } + _, err = actuator.osClient.UpdateContainer(ctx, osResource.Name, updateOpts) + if err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating container metadata: "+err.Error(), err) + } + return progress.WrapError(err) + } + + return progress.NeedsRefresh() } type swiftcontainerHelperFactory struct{} var _ helperFactory = swiftcontainerHelperFactory{} -func newActuator(ctx context.Context, orcObject *orcv1alpha1.SwiftContainer, controller interfaces.ResourceController) (swiftcontainerActuator, progress.ReconcileStatus) { +func newActuator(ctx context.Context, orcObject *orcv1alpha1.SwiftContainer, controller generic.ResourceController) (swiftcontainerActuator, progress.ReconcileStatus) { log := ctrl.LoggerFrom(ctx) // Ensure credential secrets exist and have our finalizer @@ -220,8 +392,7 @@ func newActuator(ctx context.Context, orcObject *orcv1alpha1.SwiftContainer, con } return swiftcontainerActuator{ - osClient: osClient, - k8sClient: controller.GetK8sClient(), + osClient: osClient, }, nil } @@ -229,10 +400,10 @@ func (swiftcontainerHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI return swiftcontainerAdapter{obj} } -func (swiftcontainerHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) { +func (swiftcontainerHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) (createResourceActuator, progress.ReconcileStatus) { return newActuator(ctx, orcObject, controller) } -func (swiftcontainerHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) { +func (swiftcontainerHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller generic.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) { return newActuator(ctx, orcObject, controller) } diff --git a/internal/controllers/swiftcontainer/controller.go b/internal/controllers/swiftcontainer/controller.go index 8f3b759db..968362f33 100644 --- a/internal/controllers/swiftcontainer/controller.go +++ b/internal/controllers/swiftcontainer/controller.go @@ -1,5 +1,5 @@ /* -Copyright The ORC Authors. +Copyright 2024 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. diff --git a/internal/controllers/swiftcontainer/status.go b/internal/controllers/swiftcontainer/status.go index c467380b0..03df95604 100644 --- a/internal/controllers/swiftcontainer/status.go +++ b/internal/controllers/swiftcontainer/status.go @@ -1,5 +1,5 @@ /* -Copyright The ORC Authors. +Copyright 2024 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. @@ -17,6 +17,9 @@ limitations under the License. package swiftcontainer import ( + "sort" + "strings" + "github.com/go-logr/logr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -31,32 +34,64 @@ type swiftcontainerStatusWriter struct{} type objectApplyT = orcapplyconfigv1alpha1.SwiftContainerApplyConfiguration type statusApplyT = orcapplyconfigv1alpha1.SwiftContainerStatusApplyConfiguration -var _ interfaces.ResourceStatusWriter[*orcv1alpha1.SwiftContainer, *osResourceT, *objectApplyT, *statusApplyT] = swiftcontainerStatusWriter{} +var _ interfaces.ResourceStatusWriter[*orcv1alpha1.SwiftContainer, *osContainerT, *objectApplyT, *statusApplyT] = swiftcontainerStatusWriter{} func (swiftcontainerStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT { return orcapplyconfigv1alpha1.SwiftContainer(name, namespace) } -func (swiftcontainerStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.SwiftContainer, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) { +func (swiftcontainerStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.SwiftContainer, osResource *osContainerT) (metav1.ConditionStatus, progress.ReconcileStatus) { if osResource == nil { if orcObject.Status.ID == nil { return metav1.ConditionFalse, nil - } else { - return metav1.ConditionUnknown, nil } + return metav1.ConditionUnknown, nil } + + // SwiftContainer is available as soon as it exists return metav1.ConditionTrue, nil } -func (swiftcontainerStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { +func (swiftcontainerStatusWriter) ApplyResourceStatus(_ logr.Logger, osResource *osContainerT, statusApply *statusApplyT) { resourceStatus := orcapplyconfigv1alpha1.SwiftContainerResourceStatus(). - WithName(osResource.Name) + WithName(osResource.Name). + WithBytesUsed(osResource.BytesUsed). + WithObjectCount(osResource.ObjectCount) - // TODO(scaffolding): add all of the fields supported in the SwiftContainerResourceStatus struct - // If a zero-value isn't expected in the response, place it behind a conditional + // containerRead and containerWrite are stored as []string (parsed from + // comma-separated ACL headers); join them back for status reporting. + // Only set the status field if the resulting ACL string is non-empty; + // gophercloud may return []string{""} for an absent header, which + // should not be surfaced as an empty-string ACL in status. + if readACL := strings.Join(osResource.Read, ","); readACL != "" { + resourceStatus.WithContainerRead(readACL) + } + if writeACL := strings.Join(osResource.Write, ","); writeACL != "" { + resourceStatus.WithContainerWrite(writeACL) + } - if osResource.Description != "" { - resourceStatus.WithDescription(osResource.Description) + if osResource.StoragePolicy != "" { + resourceStatus.WithStoragePolicy(osResource.StoragePolicy) + } + if osResource.VersionsLocation != "" { + resourceStatus.WithVersions(osResource.VersionsLocation) + } + + // Populate observed metadata from X-Container-Meta-* headers. + // Keys are sorted to ensure deterministic output. + if len(osResource.Metadata) > 0 { + keys := make([]string, 0, len(osResource.Metadata)) + for k := range osResource.Metadata { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + resourceStatus.WithMetadata( + orcapplyconfigv1alpha1.SwiftContainerMetadataStatus(). + WithKey(k). + WithValue(osResource.Metadata[k]), + ) + } } statusApply.WithResource(resourceStatus) diff --git a/internal/controllers/swiftcontainer/zz_generated.adapter.go b/internal/controllers/swiftcontainer/zz_generated.adapter.go new file mode 100644 index 000000000..9dcb12416 --- /dev/null +++ b/internal/controllers/swiftcontainer/zz_generated.adapter.go @@ -0,0 +1,92 @@ +// 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 swiftcontainer + +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.SwiftContainer + orcObjectListT = orcv1alpha1.SwiftContainerList + resourceSpecT = orcv1alpha1.SwiftContainerResourceSpec + filterT = orcv1alpha1.SwiftContainerFilter +) + +// Derived types +type ( + orcObjectPT = *orcObjectT + adapterI = interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT] + adapterT = swiftcontainerAdapter +) + +type swiftcontainerAdapter struct { + *orcv1alpha1.SwiftContainer +} + +var _ adapterI = &adapterT{} + +func (f adapterT) GetObject() orcObjectPT { + return f.SwiftContainer +} + +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 + } + if f.Spec.Import.Name == nil { + return nil + } + name := string(*f.Spec.Import.Name) + return &name +} + +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/swiftcontainer/zz_generated.controller.go b/internal/controllers/swiftcontainer/zz_generated.controller.go new file mode 100644 index 000000000..12a7d511b --- /dev/null +++ b/internal/controllers/swiftcontainer/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 swiftcontainer + +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/mock/doc.go b/internal/osclients/mock/doc.go index 5ee7aa5da..205d540b9 100644 --- a/internal/osclients/mock/doc.go +++ b/internal/osclients/mock/doc.go @@ -62,6 +62,9 @@ import ( //go:generate mockgen -package mock -destination=sharenetwork.go -source=../sharenetwork.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock ShareNetworkClient //go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt sharenetwork.go > _sharenetwork.go && mv _sharenetwork.go sharenetwork.go" +//go:generate mockgen -package mock -destination=swiftcontainer.go -source=../swiftcontainer.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock SwiftContainerClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt swiftcontainer.go > _swiftcontainer.go && mv _swiftcontainer.go swiftcontainer.go" + //go:generate mockgen -package mock -destination=user.go -source=../user.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock UserClient //go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt user.go > _user.go && mv _user.go user.go" diff --git a/internal/osclients/mock/swiftcontainer.go b/internal/osclients/mock/swiftcontainer.go new file mode 100644 index 000000000..3667e4e3e --- /dev/null +++ b/internal/osclients/mock/swiftcontainer.go @@ -0,0 +1,146 @@ +/* +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: ../swiftcontainer.go +// +// Generated by this command: +// +// mockgen -package mock -destination=swiftcontainer.go -source=../swiftcontainer.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock SwiftContainerClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + iter "iter" + reflect "reflect" + + containers "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" + gomock "go.uber.org/mock/gomock" +) + +// MockSwiftContainerClient is a mock of SwiftContainerClient interface. +type MockSwiftContainerClient struct { + ctrl *gomock.Controller + recorder *MockSwiftContainerClientMockRecorder + isgomock struct{} +} + +// MockSwiftContainerClientMockRecorder is the mock recorder for MockSwiftContainerClient. +type MockSwiftContainerClientMockRecorder struct { + mock *MockSwiftContainerClient +} + +// NewMockSwiftContainerClient creates a new mock instance. +func NewMockSwiftContainerClient(ctrl *gomock.Controller) *MockSwiftContainerClient { + mock := &MockSwiftContainerClient{ctrl: ctrl} + mock.recorder = &MockSwiftContainerClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSwiftContainerClient) EXPECT() *MockSwiftContainerClientMockRecorder { + return m.recorder +} + +// CreateContainer mocks base method. +func (m *MockSwiftContainerClient) CreateContainer(ctx context.Context, containerName string, opts containers.CreateOptsBuilder) (*containers.CreateHeader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateContainer", ctx, containerName, opts) + ret0, _ := ret[0].(*containers.CreateHeader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateContainer indicates an expected call of CreateContainer. +func (mr *MockSwiftContainerClientMockRecorder) CreateContainer(ctx, containerName, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateContainer", reflect.TypeOf((*MockSwiftContainerClient)(nil).CreateContainer), ctx, containerName, opts) +} + +// DeleteContainer mocks base method. +func (m *MockSwiftContainerClient) DeleteContainer(ctx context.Context, containerName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteContainer", ctx, containerName) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteContainer indicates an expected call of DeleteContainer. +func (mr *MockSwiftContainerClientMockRecorder) DeleteContainer(ctx, containerName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteContainer", reflect.TypeOf((*MockSwiftContainerClient)(nil).DeleteContainer), ctx, containerName) +} + +// GetContainer mocks base method. +func (m *MockSwiftContainerClient) GetContainer(ctx context.Context, containerName string, opts containers.GetOptsBuilder) (*containers.GetHeader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetContainer", ctx, containerName, opts) + ret0, _ := ret[0].(*containers.GetHeader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetContainer indicates an expected call of GetContainer. +func (mr *MockSwiftContainerClientMockRecorder) GetContainer(ctx, containerName, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainer", reflect.TypeOf((*MockSwiftContainerClient)(nil).GetContainer), ctx, containerName, opts) +} + +// GetContainerMetadata mocks base method. +func (m *MockSwiftContainerClient) GetContainerMetadata(ctx context.Context, containerName string) (map[string]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetContainerMetadata", ctx, containerName) + ret0, _ := ret[0].(map[string]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetContainerMetadata indicates an expected call of GetContainerMetadata. +func (mr *MockSwiftContainerClientMockRecorder) GetContainerMetadata(ctx, containerName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContainerMetadata", reflect.TypeOf((*MockSwiftContainerClient)(nil).GetContainerMetadata), ctx, containerName) +} + +// ListContainers mocks base method. +func (m *MockSwiftContainerClient) ListContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListContainers", ctx, listOpts) + ret0, _ := ret[0].(iter.Seq2[*containers.Container, error]) + return ret0 +} + +// ListContainers indicates an expected call of ListContainers. +func (mr *MockSwiftContainerClientMockRecorder) ListContainers(ctx, listOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContainers", reflect.TypeOf((*MockSwiftContainerClient)(nil).ListContainers), ctx, listOpts) +} + +// UpdateContainer mocks base method. +func (m *MockSwiftContainerClient) UpdateContainer(ctx context.Context, containerName string, opts containers.UpdateOptsBuilder) (*containers.UpdateHeader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateContainer", ctx, containerName, opts) + ret0, _ := ret[0].(*containers.UpdateHeader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateContainer indicates an expected call of UpdateContainer. +func (mr *MockSwiftContainerClientMockRecorder) UpdateContainer(ctx, containerName, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContainer", reflect.TypeOf((*MockSwiftContainerClient)(nil).UpdateContainer), ctx, containerName, opts) +} diff --git a/internal/osclients/swiftcontainer.go b/internal/osclients/swiftcontainer.go index 9fffd783c..74703742e 100644 --- a/internal/osclients/swiftcontainer.go +++ b/internal/osclients/swiftcontainer.go @@ -27,17 +27,19 @@ import ( "github.com/gophercloud/utils/v2/openstack/clientconfig" ) +// SwiftContainerClient is an interface for interacting with OpenStack Swift containers. type SwiftContainerClient interface { - ListSwiftContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] - CreateSwiftContainer(ctx context.Context, opts containers.CreateOptsBuilder) (*containers.Container, error) - DeleteSwiftContainer(ctx context.Context, resourceID string) error - GetSwiftContainer(ctx context.Context, resourceID string) (*containers.Container, error) - UpdateSwiftContainer(ctx context.Context, id string, opts containers.UpdateOptsBuilder) (*containers.Container, error) + ListContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] + CreateContainer(ctx context.Context, containerName string, opts containers.CreateOptsBuilder) (*containers.CreateHeader, error) + GetContainer(ctx context.Context, containerName string, opts containers.GetOptsBuilder) (*containers.GetHeader, error) + GetContainerMetadata(ctx context.Context, containerName string) (map[string]string, error) + DeleteContainer(ctx context.Context, containerName string) error + UpdateContainer(ctx context.Context, containerName string, opts containers.UpdateOptsBuilder) (*containers.UpdateHeader, error) } -type swiftcontainerClient struct{ client *gophercloud.ServiceClient } +type swiftContainerClient struct{ client *gophercloud.ServiceClient } -// NewSwiftContainerClient returns a new OpenStack client. +// NewSwiftContainerClient returns a new OpenStack Swift object storage client. func NewSwiftContainerClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (SwiftContainerClient, error) { client, err := openstack.NewObjectStorageV1(providerClient, gophercloud.EndpointOpts{ Region: providerClientOpts.RegionName, @@ -48,57 +50,72 @@ func NewSwiftContainerClient(providerClient *gophercloud.ProviderClient, provide return nil, fmt.Errorf("failed to create swiftcontainer service client: %v", err) } - return &swiftcontainerClient{client}, nil + return NewSwiftContainerClientFromServiceClient(client), nil } -func (c swiftcontainerClient) ListSwiftContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { +// NewSwiftContainerClientFromServiceClient returns a SwiftContainerClient wrapping +// the given gophercloud ServiceClient. This is primarily useful for testing. +func NewSwiftContainerClientFromServiceClient(client *gophercloud.ServiceClient) SwiftContainerClient { + return &swiftContainerClient{client} +} + +func (c swiftContainerClient) ListContainers(ctx context.Context, listOpts containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { pager := containers.List(c.client, listOpts) return func(yield func(*containers.Container, error) bool) { - _ = pager.EachPage(ctx, yieldPage(containers.ExtractContainers, yield)) + _ = pager.EachPage(ctx, yieldPage(containers.ExtractInfo, yield)) } } -func (c swiftcontainerClient) CreateSwiftContainer(ctx context.Context, opts containers.CreateOptsBuilder) (*containers.Container, error) { - return containers.Create(ctx, c.client, opts).Extract() +func (c swiftContainerClient) CreateContainer(ctx context.Context, containerName string, opts containers.CreateOptsBuilder) (*containers.CreateHeader, error) { + return containers.Create(ctx, c.client, containerName, opts).Extract() } -func (c swiftcontainerClient) DeleteSwiftContainer(ctx context.Context, resourceID string) error { - return containers.Delete(ctx, c.client, resourceID).ExtractErr() +func (c swiftContainerClient) GetContainer(ctx context.Context, containerName string, opts containers.GetOptsBuilder) (*containers.GetHeader, error) { + return containers.Get(ctx, c.client, containerName, opts).Extract() } -func (c swiftcontainerClient) GetSwiftContainer(ctx context.Context, resourceID string) (*containers.Container, error) { - return containers.Get(ctx, c.client, resourceID).Extract() +func (c swiftContainerClient) GetContainerMetadata(ctx context.Context, containerName string) (map[string]string, error) { + return containers.Get(ctx, c.client, containerName, nil).ExtractMetadata() } -func (c swiftcontainerClient) UpdateSwiftContainer(ctx context.Context, id string, opts containers.UpdateOptsBuilder) (*containers.Container, error) { - return containers.Update(ctx, c.client, id, opts).Extract() +func (c swiftContainerClient) DeleteContainer(ctx context.Context, containerName string) error { + _, err := containers.Delete(ctx, c.client, containerName).Extract() + return err } -type swiftcontainerErrorClient struct{ error } +func (c swiftContainerClient) UpdateContainer(ctx context.Context, containerName string, opts containers.UpdateOptsBuilder) (*containers.UpdateHeader, error) { + return containers.Update(ctx, c.client, containerName, opts).Extract() +} + +type swiftContainerErrorClient struct{ error } // NewSwiftContainerErrorClient returns a SwiftContainerClient in which every method returns the given error. func NewSwiftContainerErrorClient(e error) SwiftContainerClient { - return swiftcontainerErrorClient{e} + return swiftContainerErrorClient{e} } -func (e swiftcontainerErrorClient) ListSwiftContainers(_ context.Context, _ containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { +func (e swiftContainerErrorClient) ListContainers(_ context.Context, _ containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { return func(yield func(*containers.Container, error) bool) { yield(nil, e.error) } } -func (e swiftcontainerErrorClient) CreateSwiftContainer(_ context.Context, _ containers.CreateOptsBuilder) (*containers.Container, error) { +func (e swiftContainerErrorClient) CreateContainer(_ context.Context, _ string, _ containers.CreateOptsBuilder) (*containers.CreateHeader, error) { return nil, e.error } -func (e swiftcontainerErrorClient) DeleteSwiftContainer(_ context.Context, _ string) error { - return e.error +func (e swiftContainerErrorClient) GetContainer(_ context.Context, _ string, _ containers.GetOptsBuilder) (*containers.GetHeader, error) { + return nil, e.error } -func (e swiftcontainerErrorClient) GetSwiftContainer(_ context.Context, _ string) (*containers.Container, error) { +func (e swiftContainerErrorClient) GetContainerMetadata(_ context.Context, _ string) (map[string]string, error) { return nil, e.error } -func (e swiftcontainerErrorClient) UpdateSwiftContainer(_ context.Context, _ string, _ containers.UpdateOptsBuilder) (*containers.Container, error) { +func (e swiftContainerErrorClient) DeleteContainer(_ context.Context, _ string) error { + return e.error +} + +func (e swiftContainerErrorClient) UpdateContainer(_ context.Context, _ string, _ containers.UpdateOptsBuilder) (*containers.UpdateHeader, error) { return nil, e.error } diff --git a/internal/scope/mock.go b/internal/scope/mock.go index 8ea474b64..9edacf2dc 100644 --- a/internal/scope/mock.go +++ b/internal/scope/mock.go @@ -50,6 +50,7 @@ type MockScopeFactory struct { VolumeClient *mock.MockVolumeClient VolumeTypeClient *mock.MockVolumeTypeClient ShareNetworkClient *mock.MockShareNetworkClient + SwiftContainerClient *mock.MockSwiftContainerClient clientScopeCreateError error } @@ -69,6 +70,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { serviceClient := mock.NewMockServiceClient(mockCtrl) userClient := mock.NewMockUserClient(mockCtrl) sharenetworkClient := mock.NewMockShareNetworkClient(mockCtrl) + swiftcontainerClient := mock.NewMockSwiftContainerClient(mockCtrl) volumeClient := mock.NewMockVolumeClient(mockCtrl) volumetypeClient := mock.NewMockVolumeTypeClient(mockCtrl) @@ -86,6 +88,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { RoleClient: roleClient, ServiceClient: serviceClient, ShareNetworkClient: sharenetworkClient, + SwiftContainerClient: swiftcontainerClient, UserClient: userClient, VolumeClient: volumeClient, VolumeTypeClient: volumetypeClient, @@ -147,6 +150,10 @@ func (f *MockScopeFactory) NewShareNetworkClient() (osclients.ShareNetworkClient return f.ShareNetworkClient, nil } +func (f *MockScopeFactory) NewSwiftContainerClient() (osclients.SwiftContainerClient, error) { + return f.SwiftContainerClient, nil +} + func (f *MockScopeFactory) NewKeyPairClient() (osclients.KeyPairClient, error) { return f.KeyPairClient, nil } diff --git a/internal/scope/provider.go b/internal/scope/provider.go index 1606e18a1..36ca8ab9c 100644 --- a/internal/scope/provider.go +++ b/internal/scope/provider.go @@ -189,6 +189,10 @@ func (s *providerScope) NewShareNetworkClient() (clients.ShareNetworkClient, err return clients.NewShareNetworkClient(s.providerClient, s.providerClientOpts) } +func (s *providerScope) NewSwiftContainerClient() (clients.SwiftContainerClient, error) { + return clients.NewSwiftContainerClient(s.providerClient, s.providerClientOpts) +} + func (s *providerScope) NewKeyPairClient() (clients.KeyPairClient, error) { return clients.NewKeyPairClient(s.providerClient, s.providerClientOpts) } diff --git a/internal/scope/scope.go b/internal/scope/scope.go index 0b02b79bd..3ceae1a99 100644 --- a/internal/scope/scope.go +++ b/internal/scope/scope.go @@ -61,6 +61,7 @@ type Scope interface { NewRoleClient() (osclients.RoleClient, error) NewServiceClient() (osclients.ServiceClient, error) NewShareNetworkClient() (osclients.ShareNetworkClient, error) + NewSwiftContainerClient() (osclients.SwiftContainerClient, error) NewUserClient() (osclients.UserClient, error) NewVolumeClient() (osclients.VolumeClient, error) NewVolumeTypeClient() (osclients.VolumeTypeClient, error) diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainer.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainer.go new file mode 100644 index 000000000..e82fb1b86 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainer.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" +) + +// SwiftContainerApplyConfiguration represents a declarative configuration of the SwiftContainer type for use +// with apply. +type SwiftContainerApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *SwiftContainerSpecApplyConfiguration `json:"spec,omitempty"` + Status *SwiftContainerStatusApplyConfiguration `json:"status,omitempty"` +} + +// SwiftContainer constructs a declarative configuration of the SwiftContainer type for use with +// apply. +func SwiftContainer(name, namespace string) *SwiftContainerApplyConfiguration { + b := &SwiftContainerApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("SwiftContainer") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b +} + +// ExtractSwiftContainer extracts the applied configuration owned by fieldManager from +// swiftContainer. If no managedFields are found in swiftContainer for fieldManager, a +// SwiftContainerApplyConfiguration 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. +// swiftContainer must be a unmodified SwiftContainer API object that was retrieved from the Kubernetes API. +// ExtractSwiftContainer 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 ExtractSwiftContainer(swiftContainer *apiv1alpha1.SwiftContainer, fieldManager string) (*SwiftContainerApplyConfiguration, error) { + return extractSwiftContainer(swiftContainer, fieldManager, "") +} + +// ExtractSwiftContainerStatus is the same as ExtractSwiftContainer except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractSwiftContainerStatus(swiftContainer *apiv1alpha1.SwiftContainer, fieldManager string) (*SwiftContainerApplyConfiguration, error) { + return extractSwiftContainer(swiftContainer, fieldManager, "status") +} + +func extractSwiftContainer(swiftContainer *apiv1alpha1.SwiftContainer, fieldManager string, subresource string) (*SwiftContainerApplyConfiguration, error) { + b := &SwiftContainerApplyConfiguration{} + err := managedfields.ExtractInto(swiftContainer, internal.Parser().Type("com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainer"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(swiftContainer.Name) + b.WithNamespace(swiftContainer.Namespace) + + b.WithKind("SwiftContainer") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b, nil +} +func (b SwiftContainerApplyConfiguration) 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 *SwiftContainerApplyConfiguration) WithKind(value string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithAPIVersion(value string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithName(value string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithGenerateName(value string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithNamespace(value string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithUID(value types.UID) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithResourceVersion(value string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithGeneration(value int64) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithCreationTimestamp(value metav1.Time) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithLabels(entries map[string]string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithAnnotations(entries map[string]string) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithFinalizers(values ...string) *SwiftContainerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *SwiftContainerApplyConfiguration) 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 *SwiftContainerApplyConfiguration) WithSpec(value *SwiftContainerSpecApplyConfiguration) *SwiftContainerApplyConfiguration { + 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 *SwiftContainerApplyConfiguration) WithStatus(value *SwiftContainerStatusApplyConfiguration) *SwiftContainerApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *SwiftContainerApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *SwiftContainerApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *SwiftContainerApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *SwiftContainerApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerfilter.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerfilter.go new file mode 100644 index 000000000..cda26dcfa --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerfilter.go @@ -0,0 +1,52 @@ +/* +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" +) + +// SwiftContainerFilterApplyConfiguration represents a declarative configuration of the SwiftContainerFilter type for use +// with apply. +type SwiftContainerFilterApplyConfiguration struct { + Name *apiv1alpha1.SwiftContainerName `json:"name,omitempty"` + Prefix *string `json:"prefix,omitempty"` +} + +// SwiftContainerFilterApplyConfiguration constructs a declarative configuration of the SwiftContainerFilter type for use with +// apply. +func SwiftContainerFilter() *SwiftContainerFilterApplyConfiguration { + return &SwiftContainerFilterApplyConfiguration{} +} + +// 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 *SwiftContainerFilterApplyConfiguration) WithName(value apiv1alpha1.SwiftContainerName) *SwiftContainerFilterApplyConfiguration { + b.Name = &value + return b +} + +// WithPrefix sets the Prefix 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 Prefix field is set to the value of the last call. +func (b *SwiftContainerFilterApplyConfiguration) WithPrefix(value string) *SwiftContainerFilterApplyConfiguration { + b.Prefix = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerimport.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerimport.go new file mode 100644 index 000000000..3e5d69df5 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerimport.go @@ -0,0 +1,52 @@ +/* +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" +) + +// SwiftContainerImportApplyConfiguration represents a declarative configuration of the SwiftContainerImport type for use +// with apply. +type SwiftContainerImportApplyConfiguration struct { + Name *apiv1alpha1.SwiftContainerName `json:"name,omitempty"` + Filter *SwiftContainerFilterApplyConfiguration `json:"filter,omitempty"` +} + +// SwiftContainerImportApplyConfiguration constructs a declarative configuration of the SwiftContainerImport type for use with +// apply. +func SwiftContainerImport() *SwiftContainerImportApplyConfiguration { + return &SwiftContainerImportApplyConfiguration{} +} + +// 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 *SwiftContainerImportApplyConfiguration) WithName(value apiv1alpha1.SwiftContainerName) *SwiftContainerImportApplyConfiguration { + b.Name = &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 *SwiftContainerImportApplyConfiguration) WithFilter(value *SwiftContainerFilterApplyConfiguration) *SwiftContainerImportApplyConfiguration { + b.Filter = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go new file mode 100644 index 000000000..ea014f10f --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.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 + +// SwiftContainerMetadataApplyConfiguration represents a declarative configuration of the SwiftContainerMetadata type for use +// with apply. +type SwiftContainerMetadataApplyConfiguration struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +} + +// SwiftContainerMetadataApplyConfiguration constructs a declarative configuration of the SwiftContainerMetadata type for use with +// apply. +func SwiftContainerMetadata() *SwiftContainerMetadataApplyConfiguration { + return &SwiftContainerMetadataApplyConfiguration{} +} + +// WithKey sets the Key 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 Key field is set to the value of the last call. +func (b *SwiftContainerMetadataApplyConfiguration) WithKey(value string) *SwiftContainerMetadataApplyConfiguration { + b.Key = &value + return b +} + +// WithValue sets the Value 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 Value field is set to the value of the last call. +func (b *SwiftContainerMetadataApplyConfiguration) WithValue(value string) *SwiftContainerMetadataApplyConfiguration { + b.Value = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go new file mode 100644 index 000000000..65f60268b --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.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 + +// SwiftContainerMetadataStatusApplyConfiguration represents a declarative configuration of the SwiftContainerMetadataStatus type for use +// with apply. +type SwiftContainerMetadataStatusApplyConfiguration struct { + Key *string `json:"key,omitempty"` + Value *string `json:"value,omitempty"` +} + +// SwiftContainerMetadataStatusApplyConfiguration constructs a declarative configuration of the SwiftContainerMetadataStatus type for use with +// apply. +func SwiftContainerMetadataStatus() *SwiftContainerMetadataStatusApplyConfiguration { + return &SwiftContainerMetadataStatusApplyConfiguration{} +} + +// WithKey sets the Key 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 Key field is set to the value of the last call. +func (b *SwiftContainerMetadataStatusApplyConfiguration) WithKey(value string) *SwiftContainerMetadataStatusApplyConfiguration { + b.Key = &value + return b +} + +// WithValue sets the Value 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 Value field is set to the value of the last call. +func (b *SwiftContainerMetadataStatusApplyConfiguration) WithValue(value string) *SwiftContainerMetadataStatusApplyConfiguration { + b.Value = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcespec.go new file mode 100644 index 000000000..29dc73e17 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcespec.go @@ -0,0 +1,84 @@ +/* +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" +) + +// SwiftContainerResourceSpecApplyConfiguration represents a declarative configuration of the SwiftContainerResourceSpec type for use +// with apply. +type SwiftContainerResourceSpecApplyConfiguration struct { + Name *apiv1alpha1.SwiftContainerName `json:"name,omitempty"` + Metadata []SwiftContainerMetadataApplyConfiguration `json:"metadata,omitempty"` + ContainerRead *string `json:"containerRead,omitempty"` + ContainerWrite *string `json:"containerWrite,omitempty"` + StoragePolicy *string `json:"storagePolicy,omitempty"` +} + +// SwiftContainerResourceSpecApplyConfiguration constructs a declarative configuration of the SwiftContainerResourceSpec type for use with +// apply. +func SwiftContainerResourceSpec() *SwiftContainerResourceSpecApplyConfiguration { + return &SwiftContainerResourceSpecApplyConfiguration{} +} + +// 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 *SwiftContainerResourceSpecApplyConfiguration) WithName(value apiv1alpha1.SwiftContainerName) *SwiftContainerResourceSpecApplyConfiguration { + b.Name = &value + return b +} + +// WithMetadata adds the given value to the Metadata 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 Metadata field. +func (b *SwiftContainerResourceSpecApplyConfiguration) WithMetadata(values ...*SwiftContainerMetadataApplyConfiguration) *SwiftContainerResourceSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMetadata") + } + b.Metadata = append(b.Metadata, *values[i]) + } + return b +} + +// WithContainerRead sets the ContainerRead 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 ContainerRead field is set to the value of the last call. +func (b *SwiftContainerResourceSpecApplyConfiguration) WithContainerRead(value string) *SwiftContainerResourceSpecApplyConfiguration { + b.ContainerRead = &value + return b +} + +// WithContainerWrite sets the ContainerWrite 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 ContainerWrite field is set to the value of the last call. +func (b *SwiftContainerResourceSpecApplyConfiguration) WithContainerWrite(value string) *SwiftContainerResourceSpecApplyConfiguration { + b.ContainerWrite = &value + return b +} + +// WithStoragePolicy sets the StoragePolicy 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 StoragePolicy field is set to the value of the last call. +func (b *SwiftContainerResourceSpecApplyConfiguration) WithStoragePolicy(value string) *SwiftContainerResourceSpecApplyConfiguration { + b.StoragePolicy = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go new file mode 100644 index 000000000..2f89f36b0 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go @@ -0,0 +1,125 @@ +/* +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 + +// SwiftContainerResourceStatusApplyConfiguration represents a declarative configuration of the SwiftContainerResourceStatus type for use +// with apply. +type SwiftContainerResourceStatusApplyConfiguration struct { + Name *string `json:"name,omitempty"` + BytesUsed *int64 `json:"bytesUsed,omitempty"` + ObjectCount *int64 `json:"objectCount,omitempty"` + Metadata []SwiftContainerMetadataStatusApplyConfiguration `json:"metadata,omitempty"` + ContainerRead *string `json:"containerRead,omitempty"` + ContainerWrite *string `json:"containerWrite,omitempty"` + StoragePolicy *string `json:"storagePolicy,omitempty"` + Versions *string `json:"versions,omitempty"` + QuotaBytes *int64 `json:"quotaBytes,omitempty"` + QuotaCount *int64 `json:"quotaCount,omitempty"` +} + +// SwiftContainerResourceStatusApplyConfiguration constructs a declarative configuration of the SwiftContainerResourceStatus type for use with +// apply. +func SwiftContainerResourceStatus() *SwiftContainerResourceStatusApplyConfiguration { + return &SwiftContainerResourceStatusApplyConfiguration{} +} + +// 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 *SwiftContainerResourceStatusApplyConfiguration) WithName(value string) *SwiftContainerResourceStatusApplyConfiguration { + b.Name = &value + return b +} + +// WithBytesUsed sets the BytesUsed 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 BytesUsed field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithBytesUsed(value int64) *SwiftContainerResourceStatusApplyConfiguration { + b.BytesUsed = &value + return b +} + +// WithObjectCount sets the ObjectCount 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 ObjectCount field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithObjectCount(value int64) *SwiftContainerResourceStatusApplyConfiguration { + b.ObjectCount = &value + return b +} + +// WithMetadata adds the given value to the Metadata 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 Metadata field. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithMetadata(values ...*SwiftContainerMetadataStatusApplyConfiguration) *SwiftContainerResourceStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithMetadata") + } + b.Metadata = append(b.Metadata, *values[i]) + } + return b +} + +// WithContainerRead sets the ContainerRead 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 ContainerRead field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithContainerRead(value string) *SwiftContainerResourceStatusApplyConfiguration { + b.ContainerRead = &value + return b +} + +// WithContainerWrite sets the ContainerWrite 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 ContainerWrite field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithContainerWrite(value string) *SwiftContainerResourceStatusApplyConfiguration { + b.ContainerWrite = &value + return b +} + +// WithStoragePolicy sets the StoragePolicy 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 StoragePolicy field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithStoragePolicy(value string) *SwiftContainerResourceStatusApplyConfiguration { + b.StoragePolicy = &value + return b +} + +// WithVersions sets the Versions 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 Versions field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithVersions(value string) *SwiftContainerResourceStatusApplyConfiguration { + b.Versions = &value + return b +} + +// WithQuotaBytes sets the QuotaBytes 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 QuotaBytes field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithQuotaBytes(value int64) *SwiftContainerResourceStatusApplyConfiguration { + b.QuotaBytes = &value + return b +} + +// WithQuotaCount sets the QuotaCount 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 QuotaCount field is set to the value of the last call. +func (b *SwiftContainerResourceStatusApplyConfiguration) WithQuotaCount(value int64) *SwiftContainerResourceStatusApplyConfiguration { + b.QuotaCount = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerspec.go new file mode 100644 index 000000000..077aba29e --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerspec.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" +) + +// SwiftContainerSpecApplyConfiguration represents a declarative configuration of the SwiftContainerSpec type for use +// with apply. +type SwiftContainerSpecApplyConfiguration struct { + Import *SwiftContainerImportApplyConfiguration `json:"import,omitempty"` + Resource *SwiftContainerResourceSpecApplyConfiguration `json:"resource,omitempty"` + ManagementPolicy *apiv1alpha1.ManagementPolicy `json:"managementPolicy,omitempty"` + ManagedOptions *ManagedOptionsApplyConfiguration `json:"managedOptions,omitempty"` + CloudCredentialsRef *CloudCredentialsReferenceApplyConfiguration `json:"cloudCredentialsRef,omitempty"` +} + +// SwiftContainerSpecApplyConfiguration constructs a declarative configuration of the SwiftContainerSpec type for use with +// apply. +func SwiftContainerSpec() *SwiftContainerSpecApplyConfiguration { + return &SwiftContainerSpecApplyConfiguration{} +} + +// 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 *SwiftContainerSpecApplyConfiguration) WithImport(value *SwiftContainerImportApplyConfiguration) *SwiftContainerSpecApplyConfiguration { + 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 *SwiftContainerSpecApplyConfiguration) WithResource(value *SwiftContainerResourceSpecApplyConfiguration) *SwiftContainerSpecApplyConfiguration { + 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 *SwiftContainerSpecApplyConfiguration) WithManagementPolicy(value apiv1alpha1.ManagementPolicy) *SwiftContainerSpecApplyConfiguration { + 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 *SwiftContainerSpecApplyConfiguration) WithManagedOptions(value *ManagedOptionsApplyConfiguration) *SwiftContainerSpecApplyConfiguration { + 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 *SwiftContainerSpecApplyConfiguration) WithCloudCredentialsRef(value *CloudCredentialsReferenceApplyConfiguration) *SwiftContainerSpecApplyConfiguration { + b.CloudCredentialsRef = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerstatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerstatus.go new file mode 100644 index 000000000..863c8da49 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerstatus.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" +) + +// SwiftContainerStatusApplyConfiguration represents a declarative configuration of the SwiftContainerStatus type for use +// with apply. +type SwiftContainerStatusApplyConfiguration struct { + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` + ID *string `json:"id,omitempty"` + Resource *SwiftContainerResourceStatusApplyConfiguration `json:"resource,omitempty"` +} + +// SwiftContainerStatusApplyConfiguration constructs a declarative configuration of the SwiftContainerStatus type for use with +// apply. +func SwiftContainerStatus() *SwiftContainerStatusApplyConfiguration { + return &SwiftContainerStatusApplyConfiguration{} +} + +// 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 *SwiftContainerStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *SwiftContainerStatusApplyConfiguration { + 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 *SwiftContainerStatusApplyConfiguration) WithID(value string) *SwiftContainerStatusApplyConfiguration { + 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 *SwiftContainerStatusApplyConfiguration) WithResource(value *SwiftContainerResourceStatusApplyConfiguration) *SwiftContainerStatusApplyConfiguration { + b.Resource = value + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 10461ed9c..2f42dcd6f 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -3531,6 +3531,157 @@ var schemaYAML = typed.YAMLObject(`types: - name: resource type: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SubnetResourceStatus +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainer + 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.SwiftContainerSpec + default: {} + - name: status + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerStatus + default: {} +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerFilter + map: + fields: + - name: name + type: + scalar: string + - name: prefix + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerImport + map: + fields: + - name: filter + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerFilter + - name: name + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadata + map: + fields: + - name: key + type: + scalar: string + - name: value + type: + scalar: string + default: "" +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadataStatus + map: + fields: + - name: key + type: + scalar: string + - name: value + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerResourceSpec + map: + fields: + - name: containerRead + type: + scalar: string + - name: containerWrite + type: + scalar: string + - name: metadata + type: + list: + elementType: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadata + elementRelationship: atomic + - name: name + type: + scalar: string + - name: storagePolicy + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerResourceStatus + map: + fields: + - name: bytesUsed + type: + scalar: numeric + - name: containerRead + type: + scalar: string + - name: containerWrite + type: + scalar: string + - name: metadata + type: + list: + elementType: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadataStatus + elementRelationship: atomic + - name: name + type: + scalar: string + - name: objectCount + type: + scalar: numeric + - name: quotaBytes + type: + scalar: numeric + - name: quotaCount + type: + scalar: numeric + - name: storagePolicy + type: + scalar: string + - name: versions + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerSpec + 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.SwiftContainerImport + - 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.SwiftContainerResourceSpec +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerStatus + 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.SwiftContainerResourceStatus - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.Trunk map: fields: diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index 5d8f68cd9..e5b6d854f 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -410,6 +410,24 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.SubnetSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("SubnetStatus"): return &apiv1alpha1.SubnetStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainer"): + return &apiv1alpha1.SwiftContainerApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerFilter"): + return &apiv1alpha1.SwiftContainerFilterApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerImport"): + return &apiv1alpha1.SwiftContainerImportApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerMetadata"): + return &apiv1alpha1.SwiftContainerMetadataApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerMetadataStatus"): + return &apiv1alpha1.SwiftContainerMetadataStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerResourceSpec"): + return &apiv1alpha1.SwiftContainerResourceSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerResourceStatus"): + return &apiv1alpha1.SwiftContainerResourceStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerSpec"): + return &apiv1alpha1.SwiftContainerSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SwiftContainerStatus"): + return &apiv1alpha1.SwiftContainerStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Trunk"): return &apiv1alpha1.TrunkApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("TrunkFilter"): 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 d5c517b1e..2a85aee7c 100644 --- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go @@ -49,6 +49,7 @@ type OpenstackV1alpha1Interface interface { ServicesGetter ShareNetworksGetter SubnetsGetter + SwiftContainersGetter TrunksGetter UsersGetter VolumesGetter @@ -144,6 +145,10 @@ func (c *OpenstackV1alpha1Client) Subnets(namespace string) SubnetInterface { return newSubnets(c, namespace) } +func (c *OpenstackV1alpha1Client) SwiftContainers(namespace string) SwiftContainerInterface { + return newSwiftContainers(c, namespace) +} + func (c *OpenstackV1alpha1Client) Trunks(namespace string) TrunkInterface { return newTrunks(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 f5dcb5da4..bbde5f0d1 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 @@ -112,6 +112,10 @@ func (c *FakeOpenstackV1alpha1) Subnets(namespace string) v1alpha1.SubnetInterfa return newFakeSubnets(c, namespace) } +func (c *FakeOpenstackV1alpha1) SwiftContainers(namespace string) v1alpha1.SwiftContainerInterface { + return newFakeSwiftContainers(c, namespace) +} + func (c *FakeOpenstackV1alpha1) Trunks(namespace string) v1alpha1.TrunkInterface { return newFakeTrunks(c, namespace) } diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_swiftcontainer.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_swiftcontainer.go new file mode 100644 index 000000000..d709c4de3 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_swiftcontainer.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" +) + +// fakeSwiftContainers implements SwiftContainerInterface +type fakeSwiftContainers struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.SwiftContainer, *v1alpha1.SwiftContainerList, *apiv1alpha1.SwiftContainerApplyConfiguration] + Fake *FakeOpenstackV1alpha1 +} + +func newFakeSwiftContainers(fake *FakeOpenstackV1alpha1, namespace string) typedapiv1alpha1.SwiftContainerInterface { + return &fakeSwiftContainers{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.SwiftContainer, *v1alpha1.SwiftContainerList, *apiv1alpha1.SwiftContainerApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("swiftcontainers"), + v1alpha1.SchemeGroupVersion.WithKind("SwiftContainer"), + func() *v1alpha1.SwiftContainer { return &v1alpha1.SwiftContainer{} }, + func() *v1alpha1.SwiftContainerList { return &v1alpha1.SwiftContainerList{} }, + func(dst, src *v1alpha1.SwiftContainerList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.SwiftContainerList) []*v1alpha1.SwiftContainer { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.SwiftContainerList, items []*v1alpha1.SwiftContainer) { + 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 e13858a9c..d50a4cdcc 100644 --- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go @@ -60,6 +60,8 @@ type ShareNetworkExpansion interface{} type SubnetExpansion interface{} +type SwiftContainerExpansion interface{} + type TrunkExpansion interface{} type UserExpansion interface{} diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/swiftcontainer.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/swiftcontainer.go new file mode 100644 index 000000000..c97e80553 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/swiftcontainer.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" +) + +// SwiftContainersGetter has a method to return a SwiftContainerInterface. +// A group's client should implement this interface. +type SwiftContainersGetter interface { + SwiftContainers(namespace string) SwiftContainerInterface +} + +// SwiftContainerInterface has methods to work with SwiftContainer resources. +type SwiftContainerInterface interface { + Create(ctx context.Context, swiftContainer *apiv1alpha1.SwiftContainer, opts v1.CreateOptions) (*apiv1alpha1.SwiftContainer, error) + Update(ctx context.Context, swiftContainer *apiv1alpha1.SwiftContainer, opts v1.UpdateOptions) (*apiv1alpha1.SwiftContainer, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, swiftContainer *apiv1alpha1.SwiftContainer, opts v1.UpdateOptions) (*apiv1alpha1.SwiftContainer, 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.SwiftContainer, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.SwiftContainerList, 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.SwiftContainer, err error) + Apply(ctx context.Context, swiftContainer *applyconfigurationapiv1alpha1.SwiftContainerApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.SwiftContainer, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, swiftContainer *applyconfigurationapiv1alpha1.SwiftContainerApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.SwiftContainer, err error) + SwiftContainerExpansion +} + +// swiftContainers implements SwiftContainerInterface +type swiftContainers struct { + *gentype.ClientWithListAndApply[*apiv1alpha1.SwiftContainer, *apiv1alpha1.SwiftContainerList, *applyconfigurationapiv1alpha1.SwiftContainerApplyConfiguration] +} + +// newSwiftContainers returns a SwiftContainers +func newSwiftContainers(c *OpenstackV1alpha1Client, namespace string) *swiftContainers { + return &swiftContainers{ + gentype.NewClientWithListAndApply[*apiv1alpha1.SwiftContainer, *apiv1alpha1.SwiftContainerList, *applyconfigurationapiv1alpha1.SwiftContainerApplyConfiguration]( + "swiftcontainers", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.SwiftContainer { return &apiv1alpha1.SwiftContainer{} }, + func() *apiv1alpha1.SwiftContainerList { return &apiv1alpha1.SwiftContainerList{} }, + ), + } +} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go index 2e9f92392..dfefed1f7 100644 --- a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go +++ b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go @@ -66,6 +66,8 @@ type Interface interface { ShareNetworks() ShareNetworkInformer // Subnets returns a SubnetInformer. Subnets() SubnetInformer + // SwiftContainers returns a SwiftContainerInformer. + SwiftContainers() SwiftContainerInformer // Trunks returns a TrunkInformer. Trunks() TrunkInformer // Users returns a UserInformer. @@ -192,6 +194,11 @@ func (v *version) Subnets() SubnetInformer { return &subnetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// SwiftContainers returns a SwiftContainerInformer. +func (v *version) SwiftContainers() SwiftContainerInformer { + return &swiftContainerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Trunks returns a TrunkInformer. func (v *version) Trunks() TrunkInformer { return &trunkInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/swiftcontainer.go b/pkg/clients/informers/externalversions/api/v1alpha1/swiftcontainer.go new file mode 100644 index 000000000..683f61671 --- /dev/null +++ b/pkg/clients/informers/externalversions/api/v1alpha1/swiftcontainer.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" +) + +// SwiftContainerInformer provides access to a shared informer and lister for +// SwiftContainers. +type SwiftContainerInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.SwiftContainerLister +} + +type swiftContainerInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewSwiftContainerInformer constructs a new informer for SwiftContainer 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 NewSwiftContainerInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredSwiftContainerInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredSwiftContainerInformer constructs a new informer for SwiftContainer 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 NewFilteredSwiftContainerInformer(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().SwiftContainers(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().SwiftContainers(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().SwiftContainers(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().SwiftContainers(namespace).Watch(ctx, options) + }, + }, + &v2apiv1alpha1.SwiftContainer{}, + resyncPeriod, + indexers, + ) +} + +func (f *swiftContainerInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredSwiftContainerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *swiftContainerInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&v2apiv1alpha1.SwiftContainer{}, f.defaultInformer) +} + +func (f *swiftContainerInformer) Lister() apiv1alpha1.SwiftContainerLister { + return apiv1alpha1.NewSwiftContainerLister(f.Informer().GetIndexer()) +} diff --git a/pkg/clients/informers/externalversions/generic.go b/pkg/clients/informers/externalversions/generic.go index fb3637f1e..ed23144ef 100644 --- a/pkg/clients/informers/externalversions/generic.go +++ b/pkg/clients/informers/externalversions/generic.go @@ -95,6 +95,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().ShareNetworks().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("subnets"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Subnets().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("swiftcontainers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().SwiftContainers().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("trunks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Trunks().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("users"): diff --git a/pkg/clients/listers/api/v1alpha1/expansion_generated.go b/pkg/clients/listers/api/v1alpha1/expansion_generated.go index 4d7043581..6796bed11 100644 --- a/pkg/clients/listers/api/v1alpha1/expansion_generated.go +++ b/pkg/clients/listers/api/v1alpha1/expansion_generated.go @@ -186,6 +186,14 @@ type SubnetListerExpansion interface{} // SubnetNamespaceLister. type SubnetNamespaceListerExpansion interface{} +// SwiftContainerListerExpansion allows custom methods to be added to +// SwiftContainerLister. +type SwiftContainerListerExpansion interface{} + +// SwiftContainerNamespaceListerExpansion allows custom methods to be added to +// SwiftContainerNamespaceLister. +type SwiftContainerNamespaceListerExpansion interface{} + // TrunkListerExpansion allows custom methods to be added to // TrunkLister. type TrunkListerExpansion interface{} diff --git a/pkg/clients/listers/api/v1alpha1/swiftcontainer.go b/pkg/clients/listers/api/v1alpha1/swiftcontainer.go new file mode 100644 index 000000000..a452d1774 --- /dev/null +++ b/pkg/clients/listers/api/v1alpha1/swiftcontainer.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" +) + +// SwiftContainerLister helps list SwiftContainers. +// All objects returned here must be treated as read-only. +type SwiftContainerLister interface { + // List lists all SwiftContainers in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.SwiftContainer, err error) + // SwiftContainers returns an object that can list and get SwiftContainers. + SwiftContainers(namespace string) SwiftContainerNamespaceLister + SwiftContainerListerExpansion +} + +// swiftContainerLister implements the SwiftContainerLister interface. +type swiftContainerLister struct { + listers.ResourceIndexer[*apiv1alpha1.SwiftContainer] +} + +// NewSwiftContainerLister returns a new SwiftContainerLister. +func NewSwiftContainerLister(indexer cache.Indexer) SwiftContainerLister { + return &swiftContainerLister{listers.New[*apiv1alpha1.SwiftContainer](indexer, apiv1alpha1.Resource("swiftcontainer"))} +} + +// SwiftContainers returns an object that can list and get SwiftContainers. +func (s *swiftContainerLister) SwiftContainers(namespace string) SwiftContainerNamespaceLister { + return swiftContainerNamespaceLister{listers.NewNamespaced[*apiv1alpha1.SwiftContainer](s.ResourceIndexer, namespace)} +} + +// SwiftContainerNamespaceLister helps list and get SwiftContainers. +// All objects returned here must be treated as read-only. +type SwiftContainerNamespaceLister interface { + // List lists all SwiftContainers in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.SwiftContainer, err error) + // Get retrieves the SwiftContainer from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.SwiftContainer, error) + SwiftContainerNamespaceListerExpansion +} + +// swiftContainerNamespaceLister implements the SwiftContainerNamespaceLister +// interface. +type swiftContainerNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.SwiftContainer] +} diff --git a/test/apivalidations/swiftcontainer_test.go b/test/apivalidations/swiftcontainer_test.go deleted file mode 100644 index 5e3df77b5..000000000 --- a/test/apivalidations/swiftcontainer_test.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -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 apivalidations - -import ( - . "github.com/onsi/ginkgo/v2" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" - applyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" -) - -const ( - swiftcontainerName = "swiftcontainer" - swiftcontainerID = "265c9e4f-0f5a-46e4-9f3f-fb8de25ae120" -) - -func swiftcontainerStub(namespace *corev1.Namespace) *orcv1alpha1.SwiftContainer { - obj := &orcv1alpha1.SwiftContainer{} - obj.Name = swiftcontainerName - obj.Namespace = namespace.Name - return obj -} - -func testSwiftContainerResource() *applyconfigv1alpha1.SwiftContainerResourceSpecApplyConfiguration { - return applyconfigv1alpha1.SwiftContainerResourceSpec() -} - -func baseSwiftContainerPatch(obj client.Object) *applyconfigv1alpha1.SwiftContainerApplyConfiguration { - return applyconfigv1alpha1.SwiftContainer(obj.GetName(), obj.GetNamespace()). - WithSpec(applyconfigv1alpha1.SwiftContainerSpec(). - WithCloudCredentialsRef(testCredentials())) -} - -func testSwiftContainerImport() *applyconfigv1alpha1.SwiftContainerImportApplyConfiguration { - return applyconfigv1alpha1.SwiftContainerImport().WithID(swiftcontainerID) -} - -var _ = Describe("ORC SwiftContainer API validations", func() { - var namespace *corev1.Namespace - BeforeEach(func() { - namespace = createNamespace() - }) - - runManagementPolicyTests(func() *corev1.Namespace { return namespace }, managementPolicyTestArgs[*applyconfigv1alpha1.SwiftContainerApplyConfiguration]{ - createObject: func(ns *corev1.Namespace) client.Object { return swiftcontainerStub(ns) }, - basePatch: func(obj client.Object) *applyconfigv1alpha1.SwiftContainerApplyConfiguration { - return baseSwiftContainerPatch(obj) - }, - applyResource: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithResource(testSwiftContainerResource()) - }, - applyImport: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithImport(testSwiftContainerImport()) - }, - applyEmptyImport: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithImport(applyconfigv1alpha1.SwiftContainerImport()) - }, - applyEmptyFilter: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithImport(applyconfigv1alpha1.SwiftContainerImport().WithFilter(applyconfigv1alpha1.SwiftContainerFilter())) - }, - applyValidFilter: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithImport(applyconfigv1alpha1.SwiftContainerImport().WithFilter(applyconfigv1alpha1.SwiftContainerFilter().WithName("foo"))) - }, - applyManaged: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged) - }, - applyUnmanaged: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) - }, - applyManagedOptions: func(p *applyconfigv1alpha1.SwiftContainerApplyConfiguration) { - p.Spec.WithManagedOptions(applyconfigv1alpha1.ManagedOptions().WithOnDelete(orcv1alpha1.OnDeleteDetach)) - }, - getManagementPolicy: func(obj client.Object) orcv1alpha1.ManagementPolicy { - return obj.(*orcv1alpha1.SwiftContainer).Spec.ManagementPolicy - }, - getOnDelete: func(obj client.Object) orcv1alpha1.OnDelete { - return obj.(*orcv1alpha1.SwiftContainer).Spec.ManagedOptions.OnDelete - }, - }) - - // TODO(scaffolding): Add more resource-specific validation tests. - // Some common things to test: - // - Immutability of fields with `self == oldSelf` validation - // - Enum validation (valid and invalid values) - // - Numeric range validation (min/max bounds) - // - Tag uniqueness (if the resource has tags with listType=set) - // - Format validation (CIDR, UUID, etc.) - // - Cross-field validation rules -}) From 1e24ce7b40fe1751230190ab4c134d18d7891ed0 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Tue, 23 Jun 2026 10:31:07 +0300 Subject: [PATCH 3/7] SwiftContainer: add tests Add unit coverage for Swift container import, adoption, creation, validation, and OpenStack client behavior. Add KUTTL suites for minimal and full creation, import success and failure, updates, and name validation, and enable Swift in the E2E environment. --- .github/workflows/e2e.yaml | 2 +- .../swiftcontainer/actuator_test.go | 709 ++++++++++++++++-- .../controllers/swiftcontainer/tests/.gitkeep | 0 .../swiftcontainer-create-full/00-assert.yaml | 10 +- .../00-create-resource.yaml | 9 +- .../swiftcontainer-create-full/00-secret.yaml | 1 - .../swiftcontainer-create-full/README.md | 12 +- .../00-assert.yaml | 6 +- .../00-create-resource.yaml | 3 - .../00-secret.yaml | 1 - .../01-delete-secret.yaml | 1 - .../swiftcontainer-create-minimal/README.md | 9 +- .../00-create-resources.yaml | 10 +- .../00-secret.yaml | 1 - .../01-import-resource.yaml | 2 +- .../swiftcontainer-import-error/README.md | 8 +- .../00-import-resource.yaml | 2 - .../swiftcontainer-import/00-secret.yaml | 1 - .../swiftcontainer-import/01-assert.yaml | 2 - .../01-create-trap-resource.yaml | 10 +- .../swiftcontainer-import/02-assert.yaml | 7 +- .../02-create-resource.yaml | 5 +- .../tests/swiftcontainer-import/README.md | 19 +- .../swiftcontainer-update/00-assert.yaml | 6 +- .../00-minimal-resource.yaml | 3 - .../00-prerequisites.yaml | 5 + .../swiftcontainer-update/01-assert.yaml | 19 +- .../01-updated-resource.yaml | 9 +- .../swiftcontainer-update/02-assert.yaml | 6 +- .../02-reverted-resource.yaml | 2 +- .../tests/swiftcontainer-update/README.md | 17 +- .../swiftcontainer-validation/00-assert.yaml | 13 + .../00-secret.yaml | 1 - .../00-valid-resource.yaml | 11 + .../swiftcontainer-validation/01-assert.yaml | 23 + .../01-invalid-slash.yaml | 12 + .../tests/swiftcontainer-validation/README.md | 43 ++ internal/osclients/swiftcontainer_test.go | 444 +++++++++++ kuttl-test.yaml | 1 + 39 files changed, 1303 insertions(+), 142 deletions(-) create mode 100644 internal/controllers/swiftcontainer/tests/.gitkeep create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-prerequisites.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml rename internal/controllers/swiftcontainer/tests/{swiftcontainer-update => swiftcontainer-validation}/00-secret.yaml (98%) create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml create mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md create mode 100644 internal/osclients/swiftcontainer_test.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 8f9e0220d..446871873 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -38,7 +38,7 @@ jobs: with: enable_workaround_docker_io: 'false' branch: ${{ matrix.openstack_version }} - enabled_services: "openstack-cli-server,neutron-trunk,neutron-port-trusted-vif" + enabled_services: "openstack-cli-server,neutron-trunk,neutron-port-trusted-vif,s-proxy,s-account,s-container,s-object" conf_overrides: | enable_plugin neutron https://github.com/openstack/neutron ${{ matrix.openstack_version }} enable_plugin manila https://github.com/openstack/manila ${{ matrix.openstack_version }} diff --git a/internal/controllers/swiftcontainer/actuator_test.go b/internal/controllers/swiftcontainer/actuator_test.go index 3bbd72425..3b00c2815 100644 --- a/internal/controllers/swiftcontainer/actuator_test.go +++ b/internal/controllers/swiftcontainer/actuator_test.go @@ -1,5 +1,5 @@ /* -Copyright The ORC Authors. +Copyright 2024 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. @@ -17,103 +17,672 @@ limitations under the License. package swiftcontainer import ( + "context" + "errors" + "fmt" + "iter" + "strings" "testing" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" - orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + osclients "github.com/k-orc/openstack-resource-controller/v2/internal/osclients" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" ) -func TestNeedsUpdate(t *testing.T) { - testCases := []struct { - name string - updateOpts containers.UpdateOpts - expectChange bool +var ( + errNotImplemented = errors.New("not implemented") + errTest = errors.New("test error") +) + +// mockSwiftContainerClient implements the osclients.SwiftContainerClient +// interface for use in unit tests. +type mockSwiftContainerClient struct { + // containers is the list of containers returned by ListContainers. + containers []containers.Container + + // containerData maps container name to its simulated GetHeader and metadata. + containerData map[string]mockContainerData + + // listErr, if non-nil, is returned as an error from ListContainers. + listErr error +} + +type mockContainerData struct { + header containers.GetHeader + metadata map[string]string +} + +var _ osclients.SwiftContainerClient = &mockSwiftContainerClient{} + +func (m *mockSwiftContainerClient) ListContainers(_ context.Context, _ containers.ListOptsBuilder) iter.Seq2[*containers.Container, error] { + return func(yield func(*containers.Container, error) bool) { + if m.listErr != nil { + yield(nil, m.listErr) + return + } + for i := range m.containers { + if !yield(&m.containers[i], nil) { + return + } + } + } +} + +func (m *mockSwiftContainerClient) CreateContainer(_ context.Context, containerName string, _ containers.CreateOptsBuilder) (*containers.CreateHeader, error) { + if m.containerData == nil { + m.containerData = make(map[string]mockContainerData) + } + m.containerData[containerName] = mockContainerData{ + header: containers.GetHeader{}, + metadata: map[string]string{}, + } + return &containers.CreateHeader{}, nil +} + +func (m *mockSwiftContainerClient) GetContainer(_ context.Context, containerName string, _ containers.GetOptsBuilder) (*containers.GetHeader, error) { + if m.containerData == nil { + return nil, gophercloud.ErrResourceNotFound{Name: containerName} + } + data, ok := m.containerData[containerName] + if !ok { + return nil, gophercloud.ErrResourceNotFound{Name: containerName} + } + header := data.header + return &header, nil +} + +func (m *mockSwiftContainerClient) GetContainerMetadata(_ context.Context, containerName string) (map[string]string, error) { + if m.containerData == nil { + return nil, gophercloud.ErrResourceNotFound{Name: containerName} + } + data, ok := m.containerData[containerName] + if !ok { + return nil, gophercloud.ErrResourceNotFound{Name: containerName} + } + meta := make(map[string]string, len(data.metadata)) + for k, v := range data.metadata { + meta[k] = v + } + return meta, nil +} + +func (m *mockSwiftContainerClient) DeleteContainer(_ context.Context, _ string) error { + return errNotImplemented +} + +func (m *mockSwiftContainerClient) UpdateContainer(_ context.Context, _ string, _ containers.UpdateOptsBuilder) (*containers.UpdateHeader, error) { + return nil, errNotImplemented +} + +// containerResult holds the result of a single iteration from a container iterator. +type containerResult struct { + container *osContainerT + err error +} + +type checkFunc func([]containerResult) error + +func checks(fns ...checkFunc) []checkFunc { return fns } + +func noError(results []containerResult) error { + for _, result := range results { + if result.err != nil { + return fmt.Errorf("unexpected error: %w", result.err) + } + } + return nil +} + +func wantError(wantErr error) checkFunc { + return func(results []containerResult) error { + for _, result := range results { + if result.err == nil { + continue + } + if errors.Is(result.err, wantErr) { + return nil + } + return fmt.Errorf("unexpected error: %w", result.err) + } + // If we get here, no error was found anywhere + return nil + } +} + +func findsN(wantN int) checkFunc { + return func(results []containerResult) error { + found := len(results) + if found != wantN { + return fmt.Errorf("expected %d results, got %d", wantN, found) + } + return nil + } +} + +func findsID(wantName string) checkFunc { + return func(results []containerResult) error { + for _, result := range results { + if result.container == nil { + continue + } + if result.container.Name == wantName { + return nil + } + } + return fmt.Errorf("did not find container with name %s", wantName) + } +} + +// newSwiftContainerObject creates a SwiftContainer object for use in tests. +func newSwiftContainerObject(name string, resource *orcv1alpha1.SwiftContainerResourceSpec) *orcv1alpha1.SwiftContainer { + return &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: orcv1alpha1.SwiftContainerSpec{ + Resource: resource, + }, + } +} + +func TestListOSResourcesForImport(t *testing.T) { + for _, tc := range [...]struct { + name string + filter orcv1alpha1.SwiftContainerFilter + client osclients.SwiftContainerClient + checks []checkFunc }{ { - name: "Empty base opts", - updateOpts: containers.UpdateOpts{}, - expectChange: false, + name: "finds one by name", + filter: orcv1alpha1.SwiftContainerFilter{Name: ptr.To[orcv1alpha1.SwiftContainerName]("my-container")}, + client: &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "my-container": {header: containers.GetHeader{}, metadata: map[string]string{}}, + "other-container": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + }, + checks: checks(noError, findsID("my-container"), findsN(1)), }, { - name: "Updated opts", - updateOpts: containers.UpdateOpts{Name: ptr.To("updated")}, - expectChange: true, + name: "finds none by name", + filter: orcv1alpha1.SwiftContainerFilter{Name: ptr.To[orcv1alpha1.SwiftContainerName]("missing-container")}, + client: &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "my-container": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + }, + checks: checks(noError, findsN(0)), }, - } + { + name: "finds multiple containers matching prefix filter", + filter: orcv1alpha1.SwiftContainerFilter{Prefix: ptr.To("test-")}, + client: &mockSwiftContainerClient{ + containers: []containers.Container{ + {Name: "test-alpha"}, + {Name: "test-beta"}, + {Name: "other"}, + }, + containerData: map[string]mockContainerData{ + "test-alpha": {header: containers.GetHeader{}, metadata: map[string]string{}}, + "test-beta": {header: containers.GetHeader{}, metadata: map[string]string{}}, + "other": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + }, + checks: checks(noError, findsN(2)), + }, + { + name: "returns lister errors from client", + filter: orcv1alpha1.SwiftContainerFilter{Prefix: ptr.To("test-")}, + client: &mockSwiftContainerClient{ + listErr: errTest, + }, + checks: checks(wantError(errTest)), + }, + { + // When both Name and Prefix are set and the named container does not + // match the prefix, no results should be returned. Previously the + // prefix predicate was silently ignored in the name-lookup branch. + name: "finds none when name does not match prefix", + filter: orcv1alpha1.SwiftContainerFilter{ + Name: ptr.To[orcv1alpha1.SwiftContainerName]("prod-bucket"), + Prefix: ptr.To("test-"), + }, + client: &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "prod-bucket": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + }, + checks: checks(noError, findsN(0)), + }, + { + // When both Name and Prefix are set and the named container matches + // the prefix, the container should be found normally. + name: "finds one when name matches prefix", + filter: orcv1alpha1.SwiftContainerFilter{ + Name: ptr.To[orcv1alpha1.SwiftContainerName]("test-bucket"), + Prefix: ptr.To("test-"), + }, + client: &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "test-bucket": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + }, + checks: checks(noError, findsN(1), findsID("test-bucket")), + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + + actuator := swiftcontainerActuator{tc.client} + containerIter, _ := actuator.ListOSResourcesForImport(ctx, &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "swiftcontainer", + }, + }, tc.filter) - 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) + var results []containerResult + for container, err := range containerIter { + results = append(results, containerResult{container, err}) + } + + for _, check := range tc.checks { + if e := check(results); e != nil { + t.Error(e) + } } }) } } -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}, - } +func TestListOSResourcesForAdoption(t *testing.T) { + t.Run("finds container by resource name", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "explicit-name": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + } + actuator := swiftcontainerActuator{client} + orcObject := newSwiftContainerObject("my-object", &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To[orcv1alpha1.SwiftContainerName]("explicit-name"), + }) - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - resource := &orcv1alpha1.SwiftContainer{} - resource.Name = "object-name" - resource.Spec = orcv1alpha1.SwiftContainerSpec{ - Resource: &orcv1alpha1.SwiftContainerResourceSpec{Name: tt.newValue}, + containerIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject) + if !canAdopt { + t.Fatal("canAdopt should be true when spec.resource is set") + } + + var results []containerResult + for container, err := range containerIter { + results = append(results, containerResult{container, err}) + } + + for _, check := range checks(noError, findsID("explicit-name"), findsN(1)) { + if e := check(results); e != nil { + t.Error(e) } - osResource := &osResourceT{Name: tt.existingValue} + } + }) + + t.Run("finds container by object name when spec.resource.name is nil", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "my-object": {header: containers.GetHeader{}, metadata: map[string]string{}}, + }, + } + actuator := swiftcontainerActuator{client} + // spec.resource.name is nil, so the object's own name should be used + orcObject := newSwiftContainerObject("my-object", &orcv1alpha1.SwiftContainerResourceSpec{}) - updateOpts := containers.UpdateOpts{} - handleNameUpdate(&updateOpts, resource, osResource) + containerIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject) + if !canAdopt { + t.Fatal("canAdopt should be true when spec.resource is set") + } - got, _ := needsUpdate(updateOpts) - if got != tt.expectChange { - t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + var results []containerResult + for container, err := range containerIter { + results = append(results, containerResult{container, err}) + } + + for _, check := range checks(noError, findsID("my-object"), findsN(1)) { + if e := check(results); e != nil { + t.Error(e) } - }) + } + }) - } -} + t.Run("finds none when container does not exist", func(t *testing.T) { + ctx := context.Background() + // Empty containerData so all GetContainer calls return 404 + client := &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{}, + } + actuator := swiftcontainerActuator{client} + orcObject := newSwiftContainerObject("nonexistent", &orcv1alpha1.SwiftContainerResourceSpec{}) -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}, - } + containerIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject) + if !canAdopt { + t.Fatal("canAdopt should be true when spec.resource is set") + } - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - resource := &orcv1alpha1.SwiftContainerResourceSpec{Description: tt.newValue} - osResource := &osResourceT{Description: tt.existingValue} + var results []containerResult + for container, err := range containerIter { + results = append(results, containerResult{container, err}) + } - updateOpts := containers.UpdateOpts{} - handleDescriptionUpdate(&updateOpts, resource, osResource) + for _, check := range checks(noError, findsN(0)) { + if e := check(results); e != nil { + t.Error(e) + } + } + }) + + t.Run("returns metadata when container has metadata", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{ + containerData: map[string]mockContainerData{ + "my-object": { + header: containers.GetHeader{}, + metadata: map[string]string{"env": "prod", "team": "infra"}, + }, + }, + } + actuator := swiftcontainerActuator{client} + orcObject := newSwiftContainerObject("my-object", &orcv1alpha1.SwiftContainerResourceSpec{ + Metadata: []orcv1alpha1.SwiftContainerMetadata{ + {Key: "env", Value: "prod"}, + {Key: "team", Value: "infra"}, + }, + }) + + containerIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject) + if !canAdopt { + t.Fatal("canAdopt should be true when spec.resource is set") + } + + var results []containerResult + for container, err := range containerIter { + results = append(results, containerResult{container, err}) + } + + for _, check := range checks(noError, findsN(1)) { + if e := check(results); e != nil { + t.Error(e) + } + } - got, _ := needsUpdate(updateOpts) - if got != tt.expectChange { - t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + // Verify metadata is passed through + if len(results) == 1 && results[0].container != nil { + if results[0].container.Metadata["env"] != "prod" { + t.Errorf("expected metadata env=prod, got %q", results[0].container.Metadata["env"]) } + if results[0].container.Metadata["team"] != "infra" { + t.Errorf("expected metadata team=infra, got %q", results[0].container.Metadata["team"]) + } + } + }) + + t.Run("returns false for canAdopt when spec.resource is nil", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + // spec.resource is nil + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "my-object"}, + Spec: orcv1alpha1.SwiftContainerSpec{}, + } + + _, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject) + if canAdopt { + t.Error("canAdopt should be false when spec.resource is nil") + } + }) +} + +func TestCreateResource(t *testing.T) { + t.Run("successfully creates container with minimal config", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + orcObject := newSwiftContainerObject("my-container", &orcv1alpha1.SwiftContainerResourceSpec{}) + + result, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if reconcileStatus != nil { + t.Fatalf("unexpected reconcile status: %v", reconcileStatus) + } + if result == nil { + t.Fatal("expected non-nil result") + } + if result.Name != "my-container" { + t.Errorf("expected container name %q, got %q", "my-container", result.Name) + } + }) + + t.Run("successfully creates container with full config", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + orcObject := newSwiftContainerObject("full-container", &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To[orcv1alpha1.SwiftContainerName]("full-container"), + Metadata: []orcv1alpha1.SwiftContainerMetadata{ + {Key: "project", Value: "orc"}, + {Key: "env", Value: "test"}, + }, + ContainerRead: ptr.To(".r:*"), + ContainerWrite: ptr.To("account:user"), + StoragePolicy: ptr.To("gold"), }) - } + result, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if reconcileStatus != nil { + t.Fatalf("unexpected reconcile status: %v", reconcileStatus) + } + if result == nil { + t.Fatal("expected non-nil result") + } + if result.Name != "full-container" { + t.Errorf("expected container name %q, got %q", "full-container", result.Name) + } + }) + + t.Run("returns terminal error for invalid container name containing slash", func(t *testing.T) { + ctx := context.Background() + + // The actuator explicitly validates the container name before calling + // the Swift API. A slash in the name is caught early and returned as a + // terminal error with a message mentioning "forward slashes". + // (Kubebuilder validation normally prevents this from reaching the + // controller, but in unit tests API validation is not enforced.) + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + // Use a name that bypasses kubebuilder validation (in unit tests, API + // validation is not enforced); this simulates what would happen if a + // slash somehow reached the actuator. + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid"}, + Spec: orcv1alpha1.SwiftContainerSpec{ + Resource: &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To[orcv1alpha1.SwiftContainerName]("invalid/name"), + }, + }, + } + + result, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if result != nil { + t.Errorf("expected nil result, got %v", result) + } + if reconcileStatus == nil { + t.Fatal("expected non-nil reconcile status for terminal error") + } + _, err := reconcileStatus.NeedsReschedule() + if err == nil { + t.Error("expected error from reconcile status") + } + var termErr *orcerrors.TerminalError + if !errors.As(err, &termErr) { + t.Errorf("expected TerminalError, got %T: %v", err, err) + } + }) + + t.Run("returns terminal error for container name exceeding 256 bytes", func(t *testing.T) { + ctx := context.Background() + + // A name exceeding 256 UTF-8 bytes should cause Swift to reject it. + // Simulate this via the error client. + terminalErr := orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, + "container name exceeds maximum length") + client := osclients.NewSwiftContainerErrorClient(terminalErr) + actuator := swiftcontainerActuator{client} + longName := strings.Repeat("a", 257) + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid"}, + Spec: orcv1alpha1.SwiftContainerSpec{ + Resource: &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To[orcv1alpha1.SwiftContainerName](orcv1alpha1.SwiftContainerName(longName)), + }, + }, + } + + result, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if result != nil { + t.Errorf("expected nil result, got %v", result) + } + if reconcileStatus == nil { + t.Fatal("expected non-nil reconcile status for terminal error") + } + _, err := reconcileStatus.NeedsReschedule() + if err == nil { + t.Error("expected error from reconcile status") + } + var termErr *orcerrors.TerminalError + if !errors.As(err, &termErr) { + t.Errorf("expected TerminalError, got %T: %v", err, err) + } + }) + + t.Run("returns error when spec.resource is nil", func(t *testing.T) { + ctx := context.Background() + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "my-container"}, + Spec: orcv1alpha1.SwiftContainerSpec{}, + } + + result, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if result != nil { + t.Errorf("expected nil result, got %v", result) + } + if reconcileStatus == nil { + t.Fatal("expected non-nil reconcile status when spec.resource is nil") + } + _, err := reconcileStatus.NeedsReschedule() + if err == nil { + t.Error("expected error from reconcile status") + } + var termErr *orcerrors.TerminalError + if !errors.As(err, &termErr) { + t.Errorf("expected TerminalError, got %T: %v", err, err) + } + }) +} + +func TestContainerNameValidation(t *testing.T) { + t.Run("rejects names containing forward slash", func(t *testing.T) { + name := orcv1alpha1.SwiftContainerName("containers/bucket") + if !strings.Contains(string(name), "/") { + t.Fatal("test setup error: name should contain a slash") + } + // The actuator validates the name before calling the Swift API. + // A slash in the name causes an early terminal error with a message + // mentioning "forward slashes". (Kubebuilder pattern validation would + // normally catch this before it reaches the controller, but in unit + // tests API validation is not enforced.) + ctx := context.Background() + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid"}, + Spec: orcv1alpha1.SwiftContainerSpec{ + Resource: &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To(name), + }, + }, + } + _, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if reconcileStatus == nil { + t.Error("expected reconcile status error for name with slash") + return + } + _, err := reconcileStatus.NeedsReschedule() + var termErr *orcerrors.TerminalError + if !errors.As(err, &termErr) { + t.Errorf("expected TerminalError for invalid name, got %T: %v", err, err) + } + if !strings.Contains(termErr.Error(), "forward slashes") { + t.Errorf("expected error message to mention 'forward slashes', got: %v", termErr.Error()) + } + }) + + t.Run("rejects names exceeding 256 UTF-8 bytes", func(t *testing.T) { + longName := orcv1alpha1.SwiftContainerName(strings.Repeat("x", 257)) + if len(longName) <= 256 { + t.Fatal("test setup error: name should exceed 256 bytes") + } + ctx := context.Background() + terminalErr := orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, + "container name exceeds 256 bytes") + client := osclients.NewSwiftContainerErrorClient(terminalErr) + actuator := swiftcontainerActuator{client} + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid"}, + Spec: orcv1alpha1.SwiftContainerSpec{ + Resource: &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To(longName), + }, + }, + } + _, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if reconcileStatus == nil { + t.Error("expected reconcile status error for name exceeding 256 bytes") + return + } + _, err := reconcileStatus.NeedsReschedule() + var termErr *orcerrors.TerminalError + if !errors.As(err, &termErr) { + t.Errorf("expected TerminalError for too-long name, got %T: %v", err, err) + } + }) + + t.Run("accepts valid names at boundary (exactly 256 bytes)", func(t *testing.T) { + // A name of exactly 256 ASCII characters is valid. + exactName := orcv1alpha1.SwiftContainerName(strings.Repeat("a", 256)) + if len(exactName) != 256 { + t.Fatal("test setup error: name should be exactly 256 bytes") + } + ctx := context.Background() + client := &mockSwiftContainerClient{} + actuator := swiftcontainerActuator{client} + orcObject := &orcv1alpha1.SwiftContainer{ + ObjectMeta: metav1.ObjectMeta{Name: "boundary-test"}, + Spec: orcv1alpha1.SwiftContainerSpec{ + Resource: &orcv1alpha1.SwiftContainerResourceSpec{ + Name: ptr.To(exactName), + }, + }, + } + result, reconcileStatus := actuator.CreateResource(ctx, orcObject) + if reconcileStatus != nil { + t.Fatalf("expected no error for valid 256-byte name, got: %v", reconcileStatus) + } + if result == nil { + t.Error("expected non-nil result for valid 256-byte name") + } + }) } diff --git a/internal/controllers/swiftcontainer/tests/.gitkeep b/internal/controllers/swiftcontainer/tests/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml index 599081301..f8a06b80a 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml @@ -6,8 +6,7 @@ metadata: status: resource: name: swiftcontainer-create-full-override - description: SwiftContainer from "create full" test - # TODO(scaffolding): Add all fields the resource supports + containerRead: ".r:*,.rlistings" conditions: - type: Available status: "True" @@ -25,4 +24,9 @@ resourceRefs: ref: swiftcontainer assertAll: - celExpr: "swiftcontainer.status.id != ''" - # TODO(scaffolding): Add more checks + - celExpr: "swiftcontainer.status.id == 'swiftcontainer-create-full-override'" + - celExpr: "swiftcontainer.status.resource.name == 'swiftcontainer-create-full-override'" + - celExpr: "swiftcontainer.status.resource.containerRead == '.r:*,.rlistings'" + - celExpr: "has(swiftcontainer.status.resource.metadata)" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Environment' && m.value == 'test')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Owner' && m.value == 'orc-e2e')" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml index 1b05c6fb6..ca5005022 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml @@ -5,11 +5,14 @@ metadata: name: swiftcontainer-create-full spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed resource: name: swiftcontainer-create-full-override - description: SwiftContainer from "create full" test - # TODO(scaffolding): Add all fields the resource supports + metadata: + - key: environment + value: test + - key: owner + value: orc-e2e + containerRead: ".r:*,.rlistings" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml index 045711ee7..f0fb63e85 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-secret.yaml @@ -1,4 +1,3 @@ ---- apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md index 028caa11f..2c40e0230 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md @@ -2,9 +2,17 @@ ## Step 00 -Create a SwiftContainer using all available fields, and verify that the observed state corresponds to the spec. +Create a Swift container 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. +Validates that: +- The OpenStack resource uses the name from `spec.resource.name` when it is + specified, rather than the ORC object name (SC-002). +- Custom metadata key-value pairs are applied and reflected in + `status.resource.metadata`. +- Read ACL (`containerRead`) and write ACL (`containerWrite`) are configured + and reflected in `status.resource`. +- `Available=True` and `Progressing=False` conditions are set. ## Reference diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml index 9d9e123ea..4b433ebf8 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml @@ -6,7 +6,6 @@ metadata: status: resource: name: swiftcontainer-create-minimal - # TODO(scaffolding): Add all fields the resource supports conditions: - type: Available status: "True" @@ -24,4 +23,7 @@ resourceRefs: ref: swiftcontainer assertAll: - celExpr: "swiftcontainer.status.id != ''" - # TODO(scaffolding): Add more checks + - celExpr: "swiftcontainer.status.id == 'swiftcontainer-create-minimal'" + - celExpr: "!has(swiftcontainer.status.resource.metadata)" + - celExpr: "!has(swiftcontainer.status.resource.containerRead)" + - celExpr: "!has(swiftcontainer.status.resource.containerWrite)" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml index b0759616f..152c59c78 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-create-resource.yaml @@ -5,10 +5,7 @@ metadata: name: swiftcontainer-create-minimal spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed - # TODO(scaffolding): Only add the mandatory fields. It's possible the resource - # doesn't have mandatory fields, in that case, leave it empty. resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml index 045711ee7..f0fb63e85 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-secret.yaml @@ -1,4 +1,3 @@ ---- apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml index 1620791b9..95d2be18b 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/01-delete-secret.yaml @@ -1,4 +1,3 @@ ---- apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md index 4bf56c0c8..7a799b6c1 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/README.md @@ -2,13 +2,16 @@ ## Step 00 -Create a minimal SwiftContainer, that sets only the required fields, and verify that the observed state corresponds to the spec. +Create a minimal SwiftContainer, 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. +Also validate that the OpenStack resource uses the name of the ORC object when +`spec.resource.name` is not specified (SC-001). ## Step 01 -Try deleting the secret and ensure that it is not deleted thanks to the finalizer. +Try deleting the secret and ensure that it is not deleted thanks to the +finalizer. ## Reference diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml index 58c498267..1670beda9 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-create-resources.yaml @@ -5,13 +5,10 @@ metadata: name: swiftcontainer-import-error-external-1 spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed - resource: - description: SwiftContainer from "import error" test - # TODO(scaffolding): add any required field + resource: {} --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer @@ -19,10 +16,7 @@ metadata: name: swiftcontainer-import-error-external-2 spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed - resource: - description: SwiftContainer from "import error" test - # TODO(scaffolding): add any required field + resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml index 045711ee7..f0fb63e85 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/00-secret.yaml @@ -1,4 +1,3 @@ ---- apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml index 8f46a38c3..a4ae554b5 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/01-import-resource.yaml @@ -10,4 +10,4 @@ spec: managementPolicy: unmanaged import: filter: - description: SwiftContainer from "import error" test + prefix: swiftcontainer-import-error-external diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md index afcc914cb..f9a875fea 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import-error/README.md @@ -1,12 +1,14 @@ -# Import SwiftContainer with more than one matching resources +# Import SwiftContainer with more than one matching resource ## Step 00 -Create two SwiftContainers with identical specs. +Create two Swift containers whose names share the same prefix +(`swiftcontainer-import-error-external`). ## Step 01 -Ensure that an imported SwiftContainer with a filter matching the resources returns an error. +Ensure that an imported SwiftContainer using a prefix filter that matches both +resources returns an error. ## Reference diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml index 417dc2777..26fe9737f 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-import-resource.yaml @@ -11,5 +11,3 @@ spec: import: filter: name: swiftcontainer-import-external - description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test - # TODO(scaffolding): Add all fields supported by the filter diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml index 045711ee7..f0fb63e85 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/00-secret.yaml @@ -1,4 +1,3 @@ ---- apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml index dff8b95c2..996ee707a 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-assert.yaml @@ -15,8 +15,6 @@ status: reason: Success resource: name: swiftcontainer-import-external-not-this-one - description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test - # TODO(scaffolding): Add fields necessary to match filter --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml index a18860e22..5f2a0332e 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/01-create-trap-resource.yaml @@ -1,17 +1,15 @@ --- # This `swiftcontainer-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. +# - ensure that we can successfully create another resource whose name is a superstring +# of the import filter name (i.e. it's not being adopted by the import CR) +# - ensure that importing a resource whose name matches the filter will not pick this one. apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer metadata: name: swiftcontainer-import-external-not-this-one spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed - resource: - description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test - # TODO(scaffolding): Add fields necessary to match filter + resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml index 9c21f7cc1..e8832b15f 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml @@ -10,8 +10,13 @@ resourceRefs: kind: SwiftContainer name: swiftcontainer-import-external-not-this-one ref: swiftcontainer2 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: SwiftContainer + name: swiftcontainer-import + ref: swiftcontainerImport assertAll: - celExpr: "swiftcontainer1.status.id != swiftcontainer2.status.id" + - celExpr: "swiftcontainerImport.status.id == swiftcontainer1.status.id" --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer @@ -29,5 +34,3 @@ status: reason: Success resource: name: swiftcontainer-import-external - description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test - # TODO(scaffolding): Add all fields the resource supports diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml index 4728863b6..5d04265ed 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-create-resource.yaml @@ -5,10 +5,7 @@ metadata: name: swiftcontainer-import-external spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created cloudName: openstack secretName: openstack-clouds managementPolicy: managed - resource: - description: SwiftContainer swiftcontainer-import-external from "swiftcontainer-import" test - # TODO(scaffolding): Add fields necessary to match filter + resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md index fcaa0418f..0ed6a9aff 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/README.md @@ -2,16 +2,27 @@ ## Step 00 -Import a swiftcontainer that matches all fields in the filter, and verify it is waiting for the external resource to be created. +Import a Swift container using a name filter, and verify it is waiting for +the external resource to be created. ## Step 01 -Create a swiftcontainer 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. +Create a Swift container whose name is a superstring of the one specified in +the import filter, and otherwise matching the filter, and verify that it is +not being imported (trap pattern). ## Step 02 -Create a swiftcontainer matching the filter and verify that the observed status on the imported swiftcontainer corresponds to the spec of the created swiftcontainer. -Also, confirm that it does not adopt any swiftcontainer whose name is a superstring of its own. +Create a Swift container matching the filter and verify that the observed +status on the imported container corresponds to the spec of the created +container. Also verify that the created container didn't adopt the one whose +name is a superstring of it (filter specificity). + +Validates that: +- The import filter matches the container with exact name (not a superstring). +- `status.id` is populated with the imported container name. +- Adoption from unmanaged import to available state works without recreating + the container (SC-003). ## Reference diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml index 6a3006cba..428e3098c 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml @@ -7,7 +7,10 @@ resourceRefs: name: swiftcontainer-update ref: swiftcontainer assertAll: - - celExpr: "!has(swiftcontainer.status.resource.description)" + - celExpr: "swiftcontainer.status.id == 'swiftcontainer-update'" + - celExpr: "!has(swiftcontainer.status.resource.metadata)" + - celExpr: "!has(swiftcontainer.status.resource.containerRead)" + - celExpr: "!has(swiftcontainer.status.resource.containerWrite)" --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer @@ -16,7 +19,6 @@ metadata: status: resource: name: swiftcontainer-update - # TODO(scaffolding): Add matches for more fields conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml index 2475e6dc9..584ff2baa 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-minimal-resource.yaml @@ -5,10 +5,7 @@ metadata: name: swiftcontainer-update spec: cloudCredentialsRef: - # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created or updated cloudName: openstack secretName: openstack-clouds managementPolicy: managed - # TODO(scaffolding): Only add the mandatory fields. It's possible the resource - # doesn't have mandatory fields, in that case, leave it empty. resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-prerequisites.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-prerequisites.yaml new file mode 100644 index 000000000..f0fb63e85 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-prerequisites.yaml @@ -0,0 +1,5 @@ +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/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml index 21c2efecb..acbe9b425 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml @@ -1,13 +1,26 @@ --- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: SwiftContainer + name: swiftcontainer-update + ref: swiftcontainer +assertAll: + - celExpr: "swiftcontainer.status.id == 'swiftcontainer-update'" + - celExpr: "has(swiftcontainer.status.resource.metadata)" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Environment' && m.value == 'staging')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Team' && m.value == 'platform')" + - celExpr: "swiftcontainer.status.resource.containerRead == '.r:*,.rlistings'" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer metadata: name: swiftcontainer-update status: resource: - name: swiftcontainer-update-updated - description: swiftcontainer-update-updated - # TODO(scaffolding): match all fields that were modified + name: swiftcontainer-update + containerRead: ".r:*,.rlistings" conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml index 5e827959b..ed3f61db0 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml @@ -5,6 +5,9 @@ metadata: name: swiftcontainer-update spec: resource: - name: swiftcontainer-update-updated - description: swiftcontainer-update-updated - # TODO(scaffolding): update all mutable fields + metadata: + - key: environment + value: staging + - key: team + value: platform + containerRead: ".r:*,.rlistings" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml index 55adf7b7f..428e3098c 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml @@ -7,7 +7,10 @@ resourceRefs: name: swiftcontainer-update ref: swiftcontainer assertAll: - - celExpr: "!has(swiftcontainer.status.resource.description)" + - celExpr: "swiftcontainer.status.id == 'swiftcontainer-update'" + - celExpr: "!has(swiftcontainer.status.resource.metadata)" + - celExpr: "!has(swiftcontainer.status.resource.containerRead)" + - celExpr: "!has(swiftcontainer.status.resource.containerWrite)" --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer @@ -16,7 +19,6 @@ metadata: status: resource: name: swiftcontainer-update - # TODO(scaffolding): validate that updated fields were all reverted to their original value conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml index 2c6c253ff..3cb5c0c78 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-reverted-resource.yaml @@ -1,5 +1,5 @@ # NOTE: kuttl only does patch updates, which means we can't delete a field. -# We have to use a kubectl apply command instead. +# We have to use a kubectl replace command instead. apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md index db4e8ac12..ee8fd899c 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md @@ -2,15 +2,26 @@ ## Step 00 -Create a SwiftContainer using only mandatory fields. +Create a SwiftContainer using only the required fields (no metadata, no ACLs), +and verify that the observed state corresponds to the spec. ## Step 01 -Update all mutable fields. +Update all mutable fields: +- Add custom metadata key-value pairs +- Set a read ACL (`containerRead`) + +Verify that all updated properties are reflected in the resource status. ## Step 02 -Revert the resource to its original value and verify that the resulting object matches its state when first created. +Revert the resource to its original value (no metadata, no ACLs) and verify +the resulting object matches the initial creation state. + +Validates that: +- Clearing `containerRead` (by removing the field) removes the ACL from the container. +- Removing metadata entries removes them from the container. +- `Available=True` and `Progressing=False` conditions are set after each step. ## Reference diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml new file mode 100644 index 000000000..d1f225283 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-validation-valid +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml similarity index 98% rename from internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-secret.yaml rename to internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml index 045711ee7..f0fb63e85 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-secret.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml @@ -1,4 +1,3 @@ ---- apiVersion: kuttl.dev/v1beta1 kind: TestStep commands: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml new file mode 100644 index 000000000..d3890f4a4 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-validation-valid +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml new file mode 100644 index 000000000..29d2d8029 --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml @@ -0,0 +1,23 @@ +--- +# The valid container from step 00 must continue to be available. +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: swiftcontainer-validation-valid +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + # Verify that the invalid SwiftContainer does NOT exist in the cluster. + # The CRD pattern validation should have rejected the apply command in the + # previous step, so this object must be absent. + - script: "! kubectl get swiftcontainer swiftcontainer-validation-slash --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml new file mode 100644 index 000000000..d74920e1f --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml @@ -0,0 +1,12 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # Attempt to create a SwiftContainer with a forward slash in its name. The + # CRD pattern constraint (^[^/]+$) on SwiftContainerName rejects this at the + # Kubernetes API level, so the command is expected to fail. We set + # ignoreFailure: true so the test step does not abort; the assertion in + # 01-assert.yaml then verifies that the object was not created. + - command: >- + echo '{"apiVersion":"openstack.k-orc.cloud/v1alpha1","kind":"SwiftContainer","metadata":{"name":"swiftcontainer-validation-slash"},"spec":{"cloudCredentialsRef":{"cloudName":"openstack","secretName":"openstack-clouds"},"managementPolicy":"managed","resource":{"name":"invalid/slash/name"}}}' | kubectl apply -n ${NAMESPACE} -f - + namespaced: true + ignoreFailure: true diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md new file mode 100644 index 000000000..4df2a6bae --- /dev/null +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md @@ -0,0 +1,43 @@ +# SwiftContainer name validation + +## Step 00 + +Create a valid SwiftContainer to confirm the controller is operational, and +wait for it to become available. + +## Step 01 + +Attempt to create a SwiftContainer whose underlying OpenStack container name +contains a forward slash (`/`). The Swift object-storage specification forbids +forward slashes in container names because they are used as path separators in +the object-storage URL. + +The `SwiftContainerName` type enforces this constraint at two layers: + +1. **CRD-level validation**: The kubebuilder pattern constraint `^[^/]+$` on + the `SwiftContainerName` type rejects the object at the Kubernetes API + level. The `kubectl apply` command will fail with a validation error. + +2. **Controller-level validation** (defense-in-depth): Even if a name with a + forward slash somehow bypasses API-level validation (e.g., via a direct + etcd write), the controller detects the slash before calling the Swift API + and sets `Available=False` with reason `InvalidConfiguration` and a message + mentioning `forward slashes`. + +This test exercises layer 1: the invalid container creation is attempted with +`ignoreFailure: true` (because the API is expected to reject it), and the +assertion verifies that: +- The invalid container does **not** exist in the cluster (was rejected) +- The valid container from step 00 continues to be `Available=True` + +## Validation note + +The 256-byte name length limit is enforced only at the CRD level via a CEL +rule (`self.size() <= 256`). Testing this via KUTTL is impractical because: +- YAML/JSON strings of exactly 257 bytes are cumbersome to write in test files +- The validation is already exercised by the unit tests in `actuator_test.go` + +## Reference + +- Swift object-storage specification: container names must not contain `/` +- https://k-orc.cloud/development/writing-tests/ diff --git a/internal/osclients/swiftcontainer_test.go b/internal/osclients/swiftcontainer_test.go new file mode 100644 index 000000000..48259ade6 --- /dev/null +++ b/internal/osclients/swiftcontainer_test.go @@ -0,0 +1,444 @@ +/* +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 osclients_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + + "github.com/k-orc/openstack-resource-controller/v2/internal/osclients" +) + +const fakeToken = "test-token-abc123" + +// newFakeSwiftServer creates an httptest server and a corresponding SwiftContainerClient +// pointing to it. The caller is responsible for closing the server. +func newFakeSwiftServer(t *testing.T, mux *http.ServeMux) (*httptest.Server, osclients.SwiftContainerClient) { + t.Helper() + + server := httptest.NewServer(mux) + + // Construct a gophercloud ServiceClient directly, pointing to the test server. + // This avoids needing real OpenStack credentials. + serviceClient := &gophercloud.ServiceClient{ + ProviderClient: &gophercloud.ProviderClient{ + TokenID: fakeToken, + }, + Endpoint: server.URL + "/", + } + + client := osclients.NewSwiftContainerClientFromServiceClient(serviceClient) + return server, client +} + +// TestNewSwiftContainerClient_Success verifies the constructor creates a valid client +// with a correct endpoint when given a valid provider. +func TestNewSwiftContainerClient_Success(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + defer server.Close() + + // Provide a fake endpoint locator that returns the test server URL. + providerClient := &gophercloud.ProviderClient{ + TokenID: fakeToken, + EndpointLocator: func(eo gophercloud.EndpointOpts) (string, error) { + return server.URL + "/", nil + }, + } + + opts := &clientconfig.ClientOpts{} + client, err := osclients.NewSwiftContainerClient(providerClient, opts) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if client == nil { + t.Fatal("expected non-nil client") + } +} + +// TestNewSwiftContainerClient_InvalidEndpoint verifies that an error is returned +// when the endpoint locator returns an error (simulating an invalid or missing endpoint). +func TestNewSwiftContainerClient_InvalidEndpoint(t *testing.T) { + endpointErr := errors.New("endpoint not found") + + providerClient := &gophercloud.ProviderClient{ + TokenID: fakeToken, + EndpointLocator: func(eo gophercloud.EndpointOpts) (string, error) { + return "", endpointErr + }, + } + + opts := &clientconfig.ClientOpts{} + client, err := osclients.NewSwiftContainerClient(providerClient, opts) + if err == nil { + t.Fatal("expected error, got nil") + } + if client != nil { + t.Fatalf("expected nil client on error, got: %v", client) + } +} + +// TestListContainers_Pagination verifies that the iterator correctly yields containers +// across paginated responses. +func TestListContainers_Pagination(t *testing.T) { + mux := http.NewServeMux() + + // The Swift list containers API is paginated using the marker parameter. + // First page returns two containers; second (with marker) returns one; final returns empty. + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if got := r.Header.Get("X-Auth-Token"); got != fakeToken { + t.Errorf("expected token %q, got %q", fakeToken, got) + } + + if err := r.ParseForm(); err != nil { + t.Errorf("failed to parse form: %v", err) + } + marker := r.Form.Get("marker") + + w.Header().Set("Content-Type", "application/json") + switch marker { + case "": + _, _ = fmt.Fprint(w, `[{"name":"container-a","count":0,"bytes":0},{"name":"container-b","count":3,"bytes":1024}]`) + case "container-b": + _, _ = fmt.Fprint(w, `[]`) + default: + t.Errorf("unexpected marker: %q", marker) + w.WriteHeader(http.StatusBadRequest) + } + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + var got []string + for container, err := range client.ListContainers(ctx, containers.ListOpts{}) { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + got = append(got, container.Name) + } + + want := []string{"container-a", "container-b"} + if len(got) != len(want) { + t.Fatalf("expected %d containers, got %d: %v", len(want), len(got), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("container[%d]: expected %q, got %q", i, want[i], got[i]) + } + } +} + +// TestListContainers_Error verifies that errors from page extraction are propagated +// through the iterator correctly. +// +// When a Swift server returns a 200 response with a non-JSON content type, the +// page body cannot be parsed as a container list, so ExtractInfo returns an error. +// The iterator must propagate this error to callers rather than silently stopping. +func TestListContainers_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Return 200 with text/plain - gophercloud accepts it as a valid page + // but ExtractInfo fails to unmarshal the body as []containers.Container, + // propagating the error through yieldPage. + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprint(w, "not-valid-json-container-data") + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + var gotErr error + for _, err := range client.ListContainers(ctx, containers.ListOpts{}) { + if err != nil { + gotErr = err + break + } + } + + if gotErr == nil { + t.Fatal("expected error from iterator, got nil") + } +} + +// TestCreateContainer_Success verifies that CreateContainer sends the correct +// headers, including ACLs and custom metadata. +func TestCreateContainer_Success(t *testing.T) { + const containerName = "my-container" + + mux := http.NewServeMux() + mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + + // Verify ACL headers. + if got := r.Header.Get("X-Container-Read"); got != ".r:*" { + t.Errorf("X-Container-Read: expected %q, got %q", ".r:*", got) + } + if got := r.Header.Get("X-Container-Write"); got != "myproject:myuser" { + t.Errorf("X-Container-Write: expected %q, got %q", "myproject:myuser", got) + } + + // Verify that custom metadata is sent with the correct prefix. + if got := r.Header.Get("X-Container-Meta-Environment"); got != "production" { + t.Errorf("X-Container-Meta-Environment: expected %q, got %q", "production", got) + } + + w.Header().Set("Content-Length", "0") + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + w.Header().Set("X-Trans-Id", "tx-test-id") + w.WriteHeader(http.StatusNoContent) + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + opts := containers.CreateOpts{ + ContainerRead: ".r:*", + ContainerWrite: "myproject:myuser", + Metadata: map[string]string{ + "Environment": "production", + }, + } + header, err := client.CreateContainer(ctx, containerName, opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if header == nil { + t.Fatal("expected non-nil header") + } +} + +// TestGetContainer_Success verifies that GetContainer returns correct header information. +func TestGetContainer_Success(t *testing.T) { + const containerName = "my-container" + + mux := http.NewServeMux() + mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("expected HEAD, got %s", r.Method) + } + + w.Header().Set("X-Container-Bytes-Used", "2048") + w.Header().Set("X-Container-Object-Count", "10") + w.Header().Set("X-Container-Read", ".r:*") + w.Header().Set("X-Container-Write", "admin") + w.Header().Set("X-Storage-Policy", "gold") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("X-Trans-Id", "tx-get-test") + w.WriteHeader(http.StatusNoContent) + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + header, err := client.GetContainer(ctx, containerName, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if header == nil { + t.Fatal("expected non-nil header") + } + if header.BytesUsed != 2048 { + t.Errorf("BytesUsed: expected 2048, got %d", header.BytesUsed) + } + if header.ObjectCount != 10 { + t.Errorf("ObjectCount: expected 10, got %d", header.ObjectCount) + } + if header.StoragePolicy != "gold" { + t.Errorf("StoragePolicy: expected %q, got %q", "gold", header.StoragePolicy) + } +} + +// TestGetContainerMetadata_Success verifies that metadata extraction works correctly, +// stripping the "X-Container-Meta-" prefix from header keys. +func TestGetContainerMetadata_Success(t *testing.T) { + const containerName = "my-container" + + mux := http.NewServeMux() + mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("expected HEAD, got %s", r.Method) + } + + // Gophercloud's ExtractMetadata strips the "X-Container-Meta-" prefix + // and lowercases the key. + w.Header().Set("X-Container-Meta-Env", "staging") + w.Header().Set("X-Container-Meta-Owner", "team-infra") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusNoContent) + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + meta, err := client.GetContainerMetadata(ctx, containerName) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if meta == nil { + t.Fatal("expected non-nil metadata map") + } + + // Gophercloud strips the "X-Container-Meta-" prefix. Go's net/http + // canonicalises header keys, so "X-Container-Meta-Env" becomes key "Env". + if got := meta["Env"]; got != "staging" { + t.Errorf("metadata[Env]: expected %q, got %q", "staging", got) + } + if got := meta["Owner"]; got != "team-infra" { + t.Errorf("metadata[Owner]: expected %q, got %q", "team-infra", got) + } +} + +// TestUpdateContainer_ACLs verifies that ACL updates are sent as the correct headers. +func TestUpdateContainer_ACLs(t *testing.T) { + const containerName = "my-container" + const newReadACL = ".r:*,.rlistings" + const newWriteACL = "myproject:*" + + mux := http.NewServeMux() + mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + + if got := r.Header.Get("X-Container-Read"); got != newReadACL { + t.Errorf("X-Container-Read: expected %q, got %q", newReadACL, got) + } + if got := r.Header.Get("X-Container-Write"); got != newWriteACL { + t.Errorf("X-Container-Write: expected %q, got %q", newWriteACL, got) + } + + w.Header().Set("Content-Length", "0") + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + w.Header().Set("X-Trans-Id", "tx-update-test") + w.WriteHeader(http.StatusNoContent) + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + readACL := newReadACL + writeACL := newWriteACL + opts := containers.UpdateOpts{ + ContainerRead: &readACL, + ContainerWrite: &writeACL, + } + header, err := client.UpdateContainer(ctx, containerName, opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if header == nil { + t.Fatal("expected non-nil header") + } +} + +// TestDeleteContainer_Success verifies that the delete call succeeds and sends +// a DELETE request to the correct path. +func TestDeleteContainer_Success(t *testing.T) { + const containerName = "my-container" + + mux := http.NewServeMux() + mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + }) + + server, client := newFakeSwiftServer(t, mux) + defer server.Close() + + ctx := context.Background() + err := client.DeleteContainer(ctx, containerName) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestSwiftContainerErrorClient verifies that the error client returns the +// configured error for every method. +func TestSwiftContainerErrorClient(t *testing.T) { + testErr := errors.New("test configured error") + client := osclients.NewSwiftContainerErrorClient(testErr) + ctx := context.Background() + + t.Run("ListContainers", func(t *testing.T) { + var gotErr error + for _, err := range client.ListContainers(ctx, nil) { + gotErr = err + break + } + if !errors.Is(gotErr, testErr) { + t.Errorf("expected %v, got %v", testErr, gotErr) + } + }) + + t.Run("CreateContainer", func(t *testing.T) { + _, err := client.CreateContainer(ctx, "any", nil) + if !errors.Is(err, testErr) { + t.Errorf("expected %v, got %v", testErr, err) + } + }) + + t.Run("GetContainer", func(t *testing.T) { + _, err := client.GetContainer(ctx, "any", nil) + if !errors.Is(err, testErr) { + t.Errorf("expected %v, got %v", testErr, err) + } + }) + + t.Run("GetContainerMetadata", func(t *testing.T) { + _, err := client.GetContainerMetadata(ctx, "any") + if !errors.Is(err, testErr) { + t.Errorf("expected %v, got %v", testErr, err) + } + }) + + t.Run("DeleteContainer", func(t *testing.T) { + err := client.DeleteContainer(ctx, "any") + if !errors.Is(err, testErr) { + t.Errorf("expected %v, got %v", testErr, err) + } + }) + + t.Run("UpdateContainer", func(t *testing.T) { + _, err := client.UpdateContainer(ctx, "any", nil) + if !errors.Is(err, testErr) { + t.Errorf("expected %v, got %v", testErr, err) + } + }) +} diff --git a/kuttl-test.yaml b/kuttl-test.yaml index 71fc135ed..264580b0b 100644 --- a/kuttl-test.yaml +++ b/kuttl-test.yaml @@ -23,6 +23,7 @@ testDirs: - ./internal/controllers/service/tests/ - ./internal/controllers/sharenetwork/tests/ - ./internal/controllers/subnet/tests/ +- ./internal/controllers/swiftcontainer/tests/ - ./internal/controllers/trunk/tests/ - ./internal/controllers/user/tests/ - ./internal/controllers/volume/tests/ From 9d822c3ac0037a9a580e5630c58cf4ce1b316dab Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Tue, 23 Jun 2026 10:32:43 +0300 Subject: [PATCH 4/7] SwiftContainer: add docs and examples Document SwiftContainer in the resource matrix and generated CRD reference, and add example manifests for create and import workflows. --- README.md | 1 + examples/swiftcontainer/basic.yaml | 9 + examples/swiftcontainer/full.yaml | 24 +++ examples/swiftcontainer/import-filter.yaml | 15 ++ examples/swiftcontainer/import.yaml | 12 ++ website/docs/crd-reference.md | 196 ++++++++++++++++++++- 6 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 examples/swiftcontainer/basic.yaml create mode 100644 examples/swiftcontainer/full.yaml create mode 100644 examples/swiftcontainer/import-filter.yaml create mode 100644 examples/swiftcontainer/import.yaml diff --git a/README.md b/README.md index a5663e5fd..f0c4e86f0 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ kubectl delete -f $ORC_RELEASE | server group | | ✔ | ✔ | | service | | ✔ | ✔ | | share network | | | ◐ | +| swift container | | | ◐ | | subnet | | ◐ | ◐ | | trunk | | ✔ | ✔ | | user | | ◐ | ◐ | diff --git a/examples/swiftcontainer/basic.yaml b/examples/swiftcontainer/basic.yaml new file mode 100644 index 000000000..5c0541132 --- /dev/null +++ b/examples/swiftcontainer/basic.yaml @@ -0,0 +1,9 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: my-bucket +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + resource: {} diff --git a/examples/swiftcontainer/full.yaml b/examples/swiftcontainer/full.yaml new file mode 100644 index 000000000..2645e8726 --- /dev/null +++ b/examples/swiftcontainer/full.yaml @@ -0,0 +1,24 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: my-bucket-full +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: managed + resource: + # name sets the Swift container name. Defaults to the ORC object name if not specified. + name: my-custom-bucket + # metadata sets arbitrary key-value pairs as X-Container-Meta-* headers. + metadata: + - key: owner + value: myteam + - key: environment + value: production + # containerRead sets the X-Container-Read ACL. Use ".r:*" for public read access. + containerRead: .r:*,.rlistings + # containerWrite sets the X-Container-Write ACL. + containerWrite: myaccount:myuser + # storagePolicy sets the storage policy for this container. Immutable after creation. + storagePolicy: gold diff --git a/examples/swiftcontainer/import-filter.yaml b/examples/swiftcontainer/import-filter.yaml new file mode 100644 index 000000000..26c1a3e4a --- /dev/null +++ b/examples/swiftcontainer/import-filter.yaml @@ -0,0 +1,15 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: my-filtered-bucket +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + # filter finds an existing container by matching properties. + # The filter must match exactly one container. + filter: + # prefix filters containers whose names begin with this string. + prefix: my-bucket- diff --git a/examples/swiftcontainer/import.yaml b/examples/swiftcontainer/import.yaml new file mode 100644 index 000000000..c68362cbb --- /dev/null +++ b/examples/swiftcontainer/import.yaml @@ -0,0 +1,12 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: SwiftContainer +metadata: + name: my-existing-bucket +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + # name specifies the exact name of an existing Swift container to import. + name: my-existing-container diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 3af0f3c51..cee30646d 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -31,6 +31,7 @@ Package v1alpha1 contains API Schema definitions for the openstack v1alpha1 API - [Service](#service) - [ShareNetwork](#sharenetwork) - [Subnet](#subnet) +- [SwiftContainer](#swiftcontainer) - [Trunk](#trunk) - [User](#user) - [Volume](#volume) @@ -522,6 +523,7 @@ _Appears in:_ - [ServiceSpec](#servicespec) - [ShareNetworkSpec](#sharenetworkspec) - [SubnetSpec](#subnetspec) +- [SwiftContainerSpec](#swiftcontainerspec) - [TrunkSpec](#trunkspec) - [UserSpec](#userspec) - [VolumeSpec](#volumespec) @@ -2244,6 +2246,7 @@ _Appears in:_ - [ServiceSpec](#servicespec) - [ShareNetworkSpec](#sharenetworkspec) - [SubnetSpec](#subnetspec) +- [SwiftContainerSpec](#swiftcontainerspec) - [TrunkSpec](#trunkspec) - [UserSpec](#userspec) - [VolumeSpec](#volumespec) @@ -2284,6 +2287,7 @@ _Appears in:_ - [ServiceSpec](#servicespec) - [ShareNetworkSpec](#sharenetworkspec) - [SubnetSpec](#subnetspec) +- [SwiftContainerSpec](#swiftcontainerspec) - [TrunkSpec](#trunkspec) - [UserSpec](#userspec) - [VolumeSpec](#volumespec) @@ -2596,8 +2600,6 @@ _Appears in:_ - [ShareNetworkResourceSpec](#sharenetworkresourcespec) - [SubnetFilter](#subnetfilter) - [SubnetResourceSpec](#subnetresourcespec) -- [SwiftContainerFilter](#swiftcontainerfilter) -- [SwiftContainerResourceSpec](#swiftcontainerresourcespec) - [TrunkFilter](#trunkfilter) - [TrunkResourceSpec](#trunkresourcespec) - [UserFilter](#userfilter) @@ -4603,12 +4605,202 @@ _Appears in:_ | `resource` _[SubnetResourceStatus](#subnetresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| +#### SwiftContainer + + + +SwiftContainer is the Schema for an ORC resource. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `openstack.k-orc.cloud/v1alpha1` | | | +| `kind` _string_ | `SwiftContainer` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
| +| `spec` _[SwiftContainerSpec](#swiftcontainerspec)_ | spec specifies the desired state of the resource. | | Required: \{\}
| +| `status` _[SwiftContainerStatus](#swiftcontainerstatus)_ | status defines the observed state of the resource. | | Optional: \{\}
| + + +#### SwiftContainerFilter + + + +SwiftContainerFilter defines an existing resource by its properties + +_Validation:_ +- MinProperties: 1 + +_Appears in:_ +- [SwiftContainerImport](#swiftcontainerimport) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _[SwiftContainerName](#swiftcontainername)_ | name of the existing resource | | MaxLength: 256
MinLength: 1
Pattern: `^[^/]+$`
Optional: \{\}
| +| `prefix` _string_ | prefix filters containers by name prefix. Only containers whose names
begin with this prefix will be considered. | | MaxLength: 256
MinLength: 1
Optional: \{\}
| + + +#### SwiftContainerImport + + + +SwiftContainerImport specifies an existing resource which will be imported +instead of creating a new one. Swift containers are identified by name +rather than UUID. + +_Validation:_ +- MaxProperties: 1 +- MinProperties: 1 + +_Appears in:_ +- [SwiftContainerSpec](#swiftcontainerspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _[SwiftContainerName](#swiftcontainername)_ | name contains the name of an existing Swift container to import. Note
that when specifying an import by name, the resource MUST already exist.
The ORC object will enter an error state if the resource does not exist. | | MaxLength: 256
MinLength: 1
Pattern: `^[^/]+$`
Optional: \{\}
| +| `filter` _[SwiftContainerFilter](#swiftcontainerfilter)_ | 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
Optional: \{\}
| + + +#### SwiftContainerMetadata + + + +SwiftContainerMetadata defines a key-value pair to be set as a Swift +container metadata header (X-Container-Meta-: ). + + + +_Appears in:_ +- [SwiftContainerResourceSpec](#swiftcontainerresourcespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `key` _string_ | key is the name of the metadata item. It will be used as the suffix of
the X-Container-Meta-* header. | | MaxLength: 255
MinLength: 1
Required: \{\}
| +| `value` _string_ | value is the value of the metadata item. | | MaxLength: 255
Required: \{\}
| + + +#### SwiftContainerMetadataStatus + + + +SwiftContainerMetadataStatus represents an observed metadata key-value pair +on a Swift container. + + + +_Appears in:_ +- [SwiftContainerResourceStatus](#swiftcontainerresourcestatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `key` _string_ | key is the name of the metadata item. | | MaxLength: 255
Optional: \{\}
| +| `value` _string_ | value is the value of the metadata item. | | MaxLength: 255
Optional: \{\}
| + + +#### SwiftContainerName + +_Underlying type:_ _string_ + +SwiftContainerName is the name of a Swift container. It must be between 1 +and 256 characters long and must not contain forward slashes. + +_Validation:_ +- MaxLength: 256 +- MinLength: 1 +- Pattern: `^[^/]+$` + +_Appears in:_ +- [SwiftContainerFilter](#swiftcontainerfilter) +- [SwiftContainerImport](#swiftcontainerimport) +- [SwiftContainerResourceSpec](#swiftcontainerresourcespec) + + + +#### SwiftContainerResourceSpec + + + +SwiftContainerResourceSpec contains the desired state of a Swift container. + + + +_Appears in:_ +- [SwiftContainerSpec](#swiftcontainerspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _[SwiftContainerName](#swiftcontainername)_ | name will be the name of the created Swift container. If not specified,
the name of the ORC object will be used. The name must be unique within
the account and must not contain forward slashes. | | MaxLength: 256
MinLength: 1
Pattern: `^[^/]+$`
Optional: \{\}
| +| `metadata` _[SwiftContainerMetadata](#swiftcontainermetadata) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64
Optional: \{\}
| +| `containerRead` _string_ | containerRead sets the X-Container-Read ACL header which defines who
can read objects in the container. Common values include ".r:*" for
public read access or a comma-separated list of account/container
combinations. | | MaxLength: 256
Optional: \{\}
| +| `containerWrite` _string_ | containerWrite sets the X-Container-Write ACL header which defines who
can write objects to the container. Common values include a
comma-separated list of account/container combinations. | | MaxLength: 256
Optional: \{\}
| +| `storagePolicy` _string_ | storagePolicy is the name of the storage policy to use for this
container. If not specified, the cluster's default storage policy will
be used. This field is immutable after creation. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| + + +#### SwiftContainerResourceStatus + + + +SwiftContainerResourceStatus represents the observed state of a Swift container. +_Appears in:_ +- [SwiftContainerStatus](#swiftcontainerstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | name is the name of the Swift container. | | MaxLength: 256
Optional: \{\}
| +| `bytesUsed` _integer_ | bytesUsed is the total number of bytes stored in the container. | | Optional: \{\}
| +| `objectCount` _integer_ | objectCount is the number of objects stored in the container. | | Optional: \{\}
| +| `metadata` _[SwiftContainerMetadataStatus](#swiftcontainermetadatastatus) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64
Optional: \{\}
| +| `containerRead` _string_ | containerRead is the current X-Container-Read ACL, defining who can
read objects in the container. | | MaxLength: 1024
Optional: \{\}
| +| `containerWrite` _string_ | containerWrite is the current X-Container-Write ACL, defining who can
write objects to the container. | | MaxLength: 1024
Optional: \{\}
| +| `storagePolicy` _string_ | storagePolicy is the name of the storage policy assigned to the container. | | MaxLength: 1024
Optional: \{\}
| +| `versions` _string_ | versions is the container where object versions are stored, if versioning
is enabled on this container. | | MaxLength: 1024
Optional: \{\}
| +| `quotaBytes` _integer_ | quotaBytes is the quota on the maximum number of bytes that can be
stored in the container, if set. | | Optional: \{\}
| +| `quotaCount` _integer_ | quotaCount is the quota on the maximum number of objects that can be
stored in the container, if set. | | Optional: \{\}
| + + +#### SwiftContainerSpec + + + +SwiftContainerSpec defines the desired state of an ORC object. + + + +_Appears in:_ +- [SwiftContainer](#swiftcontainer) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `import` _[SwiftContainerImport](#swiftcontainerimport)_ | import refers to an existing OpenStack resource which will be imported instead of
creating a new one. | | MaxProperties: 1
MinProperties: 1
Optional: \{\}
| +| `resource` _[SwiftContainerResourceSpec](#swiftcontainerresourcespec)_ | 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: \{\}
| +| `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]
Optional: \{\}
| +| `managedOptions` _[ManagedOptions](#managedoptions)_ | managedOptions specifies options which may be applied to managed objects. | | Optional: \{\}
| +| `cloudCredentialsRef` _[CloudCredentialsReference](#cloudcredentialsreference)_ | cloudCredentialsRef points to a secret containing OpenStack credentials | | Required: \{\}
| + + +#### SwiftContainerStatus + + + +SwiftContainerStatus defines the observed state of an ORC resource. +_Appears in:_ +- [SwiftContainer](#swiftcontainer) + +| 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
Optional: \{\}
| +| `id` _string_ | id is the unique identifier of the OpenStack resource. | | MaxLength: 1024
Optional: \{\}
| +| `resource` _[SwiftContainerResourceStatus](#swiftcontainerresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| + + #### Trunk From c85a5dab84d3c14aa32130c507f0f4453603fe50 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Sun, 28 Jun 2026 16:53:09 +0300 Subject: [PATCH 5/7] Address SwiftContainer review feedback --- api/v1alpha1/swiftcontainer_types.go | 25 +- api/v1alpha1/zz_generated.deepcopy.go | 15 - cmd/models-schema/zz_generated.openapi.go | 21 +- ...openstack.k-orc.cloud_swiftcontainers.yaml | 23 +- examples/swiftcontainer/full.yaml | 4 +- .../controllers/swiftcontainer/actuator.go | 31 +- .../swiftcontainer/actuator_test.go | 18 +- .../controllers/swiftcontainer/controller.go | 2 +- internal/controllers/swiftcontainer/status.go | 4 +- .../swiftcontainer-create-full/00-assert.yaml | 6 +- .../00-create-resource.yaml | 5 +- .../swiftcontainer-update/01-assert.yaml | 6 +- .../01-updated-resource.yaml | 5 +- .../swiftcontainer-validation/00-assert.yaml | 13 - .../swiftcontainer-validation/00-secret.yaml | 5 - .../00-valid-resource.yaml | 11 - .../swiftcontainer-validation/01-assert.yaml | 23 - .../01-invalid-slash.yaml | 12 - .../tests/swiftcontainer-validation/README.md | 43 -- internal/osclients/swiftcontainer_test.go | 444 ------------------ .../api/v1alpha1/swiftcontainermetadata.go | 10 +- .../v1alpha1/swiftcontainermetadatastatus.go | 10 +- .../applyconfiguration/internal/internal.go | 12 +- website/docs/crd-reference.md | 8 +- 24 files changed, 109 insertions(+), 647 deletions(-) delete mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml delete mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml delete mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml delete mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml delete mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml delete mode 100644 internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md delete mode 100644 internal/osclients/swiftcontainer_test.go diff --git a/api/v1alpha1/swiftcontainer_types.go b/api/v1alpha1/swiftcontainer_types.go index a69193873..5a68c2535 100644 --- a/api/v1alpha1/swiftcontainer_types.go +++ b/api/v1alpha1/swiftcontainer_types.go @@ -27,12 +27,12 @@ type SwiftContainerName string // SwiftContainerMetadata defines a key-value pair to be set as a Swift // container metadata header (X-Container-Meta-: ). type SwiftContainerMetadata struct { - // key is the name of the metadata item. It will be used as the suffix of + // name is the name of the metadata item. It will be used as the suffix of // the X-Container-Meta-* header. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=255 // +required - Key string `json:"key,omitempty"` + Name string `json:"name,omitempty"` // value is the value of the metadata item. // +kubebuilder:validation:MaxLength:=255 @@ -43,10 +43,11 @@ type SwiftContainerMetadata struct { // SwiftContainerMetadataStatus represents an observed metadata key-value pair // on a Swift container. type SwiftContainerMetadataStatus struct { - // key is the name of the metadata item. + // name is the name of the metadata item. + // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=255 - // +optional - Key string `json:"key,omitempty"` + // +required + Name string `json:"name,omitempty"` // value is the value of the metadata item. // +kubebuilder:validation:MaxLength:=255 @@ -101,7 +102,8 @@ type SwiftContainerResourceSpec struct { // metadata is a list of key-value pairs which will be set as // X-Container-Meta-* headers on the Swift container. // +kubebuilder:validation:MaxItems:=64 - // +listType=atomic + // +listType=map + // +listMapKey=name // +optional Metadata []SwiftContainerMetadata `json:"metadata,omitempty"` @@ -109,16 +111,18 @@ type SwiftContainerResourceSpec struct { // can read objects in the container. Common values include ".r:*" for // public read access or a comma-separated list of account/container // combinations. + // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=256 // +optional - ContainerRead *string `json:"containerRead,omitempty"` + ContainerRead string `json:"containerRead,omitempty"` // containerWrite sets the X-Container-Write ACL header which defines who // can write objects to the container. Common values include a // comma-separated list of account/container combinations. + // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=256 // +optional - ContainerWrite *string `json:"containerWrite,omitempty"` + ContainerWrite string `json:"containerWrite,omitempty"` // storagePolicy is the name of the storage policy to use for this // container. If not specified, the cluster's default storage policy will @@ -127,7 +131,7 @@ type SwiftContainerResourceSpec struct { // +kubebuilder:validation:MaxLength:=255 // +optional // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="storagePolicy is immutable" - StoragePolicy *string `json:"storagePolicy,omitempty"` + StoragePolicy string `json:"storagePolicy,omitempty"` } // SwiftContainerResourceStatus represents the observed state of a Swift container. @@ -147,7 +151,8 @@ type SwiftContainerResourceStatus struct { // metadata is the list of observed metadata key-value pairs on the container. // +kubebuilder:validation:MaxItems:=64 - // +listType=atomic + // +listType=map + // +listMapKey=name // +optional Metadata []SwiftContainerMetadataStatus `json:"metadata,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1fbc5462d..e3b6f19a2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6335,21 +6335,6 @@ func (in *SwiftContainerResourceSpec) DeepCopyInto(out *SwiftContainerResourceSp *out = make([]SwiftContainerMetadata, len(*in)) copy(*out, *in) } - if in.ContainerRead != nil { - in, out := &in.ContainerRead, &out.ContainerRead - *out = new(string) - **out = **in - } - if in.ContainerWrite != nil { - in, out := &in.ContainerWrite, &out.ContainerWrite - *out = new(string) - **out = **in - } - if in.StoragePolicy != nil { - in, out := &in.StoragePolicy, &out.StoragePolicy - *out = new(string) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerResourceSpec. diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 76091a33b..cf898280c 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -11917,9 +11917,9 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata Description: "SwiftContainerMetadata defines a key-value pair to be set as a Swift container metadata header (X-Container-Meta-: ).", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "key": { + "name": { SchemaProps: spec.SchemaProps{ - Description: "key is the name of the metadata item. It will be used as the suffix of the X-Container-Meta-* header.", + Description: "name is the name of the metadata item. It will be used as the suffix of the X-Container-Meta-* header.", Type: []string{"string"}, Format: "", }, @@ -11933,7 +11933,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata }, }, }, - Required: []string{"key", "value"}, + Required: []string{"name", "value"}, }, }, } @@ -11946,9 +11946,9 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata Description: "SwiftContainerMetadataStatus represents an observed metadata key-value pair on a Swift container.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "key": { + "name": { SchemaProps: spec.SchemaProps{ - Description: "key is the name of the metadata item.", + Description: "name is the name of the metadata item.", Type: []string{"string"}, Format: "", }, @@ -11961,6 +11961,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata }, }, }, + Required: []string{"name"}, }, }, } @@ -11983,7 +11984,10 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource "metadata": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", + "x-kubernetes-list-map-keys": []interface{}{ + "name", + }, + "x-kubernetes-list-type": "map", }, }, SchemaProps: spec.SchemaProps{ @@ -12059,7 +12063,10 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource "metadata": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-list-type": "atomic", + "x-kubernetes-list-map-keys": []interface{}{ + "name", + }, + "x-kubernetes-list-type": "map", }, }, SchemaProps: spec.SchemaProps{ diff --git a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml index 7e8ff758d..863988496 100644 --- a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml @@ -166,6 +166,7 @@ spec: public read access or a comma-separated list of account/container combinations. maxLength: 256 + minLength: 1 type: string containerWrite: description: |- @@ -173,6 +174,7 @@ spec: can write objects to the container. Common values include a comma-separated list of account/container combinations. maxLength: 256 + minLength: 1 type: string metadata: description: |- @@ -183,9 +185,9 @@ spec: SwiftContainerMetadata defines a key-value pair to be set as a Swift container metadata header (X-Container-Meta-: ). properties: - key: + name: description: |- - key is the name of the metadata item. It will be used as the suffix of + name is the name of the metadata item. It will be used as the suffix of the X-Container-Meta-* header. maxLength: 255 minLength: 1 @@ -195,12 +197,14 @@ spec: maxLength: 255 type: string required: - - key + - name - value type: object maxItems: 64 type: array - x-kubernetes-list-type: atomic + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map name: description: |- name will be the name of the created Swift container. If not specified, @@ -355,18 +359,23 @@ spec: SwiftContainerMetadataStatus represents an observed metadata key-value pair on a Swift container. properties: - key: - description: key is the name of the metadata item. + name: + description: name is the name of the metadata item. maxLength: 255 + minLength: 1 type: string value: description: value is the value of the metadata item. maxLength: 255 type: string + required: + - name type: object maxItems: 64 type: array - x-kubernetes-list-type: atomic + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map name: description: name is the name of the Swift container. maxLength: 256 diff --git a/examples/swiftcontainer/full.yaml b/examples/swiftcontainer/full.yaml index 2645e8726..15830ebd8 100644 --- a/examples/swiftcontainer/full.yaml +++ b/examples/swiftcontainer/full.yaml @@ -12,9 +12,9 @@ spec: name: my-custom-bucket # metadata sets arbitrary key-value pairs as X-Container-Meta-* headers. metadata: - - key: owner + - name: owner value: myteam - - key: environment + - name: environment value: production # containerRead sets the X-Container-Read ACL. Use ".r:*" for public read access. containerRead: .r:*,.rlistings diff --git a/internal/controllers/swiftcontainer/actuator.go b/internal/controllers/swiftcontainer/actuator.go index 08500f004..f2fd169b8 100644 --- a/internal/controllers/swiftcontainer/actuator.go +++ b/internal/controllers/swiftcontainer/actuator.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The ORC Authors. +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. @@ -196,20 +196,20 @@ func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj o createOpts := containers.CreateOpts{} - if resource.ContainerRead != nil { - createOpts.ContainerRead = *resource.ContainerRead + if resource.ContainerRead != "" { + createOpts.ContainerRead = resource.ContainerRead } - if resource.ContainerWrite != nil { - createOpts.ContainerWrite = *resource.ContainerWrite + if resource.ContainerWrite != "" { + createOpts.ContainerWrite = resource.ContainerWrite } - if resource.StoragePolicy != nil { - createOpts.StoragePolicy = *resource.StoragePolicy + if resource.StoragePolicy != "" { + createOpts.StoragePolicy = resource.StoragePolicy } if len(resource.Metadata) > 0 { metadata := make(map[string]string, len(resource.Metadata)) for _, m := range resource.Metadata { - metadata[m.Key] = m.Value + metadata[m.Name] = m.Value } createOpts.Metadata = metadata } @@ -237,6 +237,9 @@ func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj o func (actuator swiftcontainerActuator) DeleteResource(ctx context.Context, _ orcObjectPT, osResource *osContainerT) progress.ReconcileStatus { err := actuator.osClient.DeleteContainer(ctx, osResource.Name) + if orcerrors.IsNotFound(err) { + return nil + } return progress.WrapError(err) } @@ -262,14 +265,8 @@ func (actuator swiftcontainerActuator) reconcileACLs(ctx context.Context, orcObj currentRead := strings.Join(osResource.Read, ",") currentWrite := strings.Join(osResource.Write, ",") - desiredRead := "" - if resource.ContainerRead != nil { - desiredRead = *resource.ContainerRead - } - desiredWrite := "" - if resource.ContainerWrite != nil { - desiredWrite = *resource.ContainerWrite - } + desiredRead := resource.ContainerRead + desiredWrite := resource.ContainerWrite if currentRead == desiredRead && currentWrite == desiredWrite { log.V(logging.Debug).Info("Container ACLs are up to date") @@ -325,7 +322,7 @@ func (actuator swiftcontainerActuator) reconcileMetadata(ctx context.Context, or // in Swift). desiredMetadata := make(map[string]string, len(resource.Metadata)) for _, m := range resource.Metadata { - desiredMetadata[strings.ToLower(m.Key)] = m.Value + desiredMetadata[strings.ToLower(m.Name)] = m.Value } // Find keys to add/update and keys to remove. diff --git a/internal/controllers/swiftcontainer/actuator_test.go b/internal/controllers/swiftcontainer/actuator_test.go index 3b00c2815..1ed7b844b 100644 --- a/internal/controllers/swiftcontainer/actuator_test.go +++ b/internal/controllers/swiftcontainer/actuator_test.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The ORC Authors. +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. @@ -150,7 +150,7 @@ func wantError(wantErr error) checkFunc { return fmt.Errorf("unexpected error: %w", result.err) } // If we get here, no error was found anywhere - return nil + return fmt.Errorf("expected error %v", wantErr) } } @@ -396,8 +396,8 @@ func TestListOSResourcesForAdoption(t *testing.T) { actuator := swiftcontainerActuator{client} orcObject := newSwiftContainerObject("my-object", &orcv1alpha1.SwiftContainerResourceSpec{ Metadata: []orcv1alpha1.SwiftContainerMetadata{ - {Key: "env", Value: "prod"}, - {Key: "team", Value: "infra"}, + {Name: "env", Value: "prod"}, + {Name: "team", Value: "infra"}, }, }) @@ -471,12 +471,12 @@ func TestCreateResource(t *testing.T) { orcObject := newSwiftContainerObject("full-container", &orcv1alpha1.SwiftContainerResourceSpec{ Name: ptr.To[orcv1alpha1.SwiftContainerName]("full-container"), Metadata: []orcv1alpha1.SwiftContainerMetadata{ - {Key: "project", Value: "orc"}, - {Key: "env", Value: "test"}, + {Name: "project", Value: "orc"}, + {Name: "env", Value: "test"}, }, - ContainerRead: ptr.To(".r:*"), - ContainerWrite: ptr.To("account:user"), - StoragePolicy: ptr.To("gold"), + ContainerRead: ".r:*", + ContainerWrite: "account:user", + StoragePolicy: "gold", }) result, reconcileStatus := actuator.CreateResource(ctx, orcObject) diff --git a/internal/controllers/swiftcontainer/controller.go b/internal/controllers/swiftcontainer/controller.go index 968362f33..8f3b759db 100644 --- a/internal/controllers/swiftcontainer/controller.go +++ b/internal/controllers/swiftcontainer/controller.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The ORC Authors. +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. diff --git a/internal/controllers/swiftcontainer/status.go b/internal/controllers/swiftcontainer/status.go index 03df95604..becdff975 100644 --- a/internal/controllers/swiftcontainer/status.go +++ b/internal/controllers/swiftcontainer/status.go @@ -1,5 +1,5 @@ /* -Copyright 2024 The ORC Authors. +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. @@ -88,7 +88,7 @@ func (swiftcontainerStatusWriter) ApplyResourceStatus(_ logr.Logger, osResource for _, k := range keys { resourceStatus.WithMetadata( orcapplyconfigv1alpha1.SwiftContainerMetadataStatus(). - WithKey(k). + WithName(k). WithValue(osResource.Metadata[k]), ) } diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml index f8a06b80a..78aeb2427 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml @@ -7,6 +7,7 @@ status: resource: name: swiftcontainer-create-full-override containerRead: ".r:*,.rlistings" + containerWrite: "openstack:*" conditions: - type: Available status: "True" @@ -27,6 +28,7 @@ assertAll: - celExpr: "swiftcontainer.status.id == 'swiftcontainer-create-full-override'" - celExpr: "swiftcontainer.status.resource.name == 'swiftcontainer-create-full-override'" - celExpr: "swiftcontainer.status.resource.containerRead == '.r:*,.rlistings'" + - celExpr: "swiftcontainer.status.resource.containerWrite == 'openstack:*'" - celExpr: "has(swiftcontainer.status.resource.metadata)" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Environment' && m.value == 'test')" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Owner' && m.value == 'orc-e2e')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Environment' && m.value == 'test')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Owner' && m.value == 'orc-e2e')" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml index ca5005022..627dc45b6 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml @@ -11,8 +11,9 @@ spec: resource: name: swiftcontainer-create-full-override metadata: - - key: environment + - name: environment value: test - - key: owner + - name: owner value: orc-e2e containerRead: ".r:*,.rlistings" + containerWrite: "openstack:*" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml index acbe9b425..285ca7121 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml @@ -9,9 +9,10 @@ resourceRefs: assertAll: - celExpr: "swiftcontainer.status.id == 'swiftcontainer-update'" - celExpr: "has(swiftcontainer.status.resource.metadata)" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Environment' && m.value == 'staging')" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Team' && m.value == 'platform')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Environment' && m.value == 'staging')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Team' && m.value == 'platform')" - celExpr: "swiftcontainer.status.resource.containerRead == '.r:*,.rlistings'" + - celExpr: "swiftcontainer.status.resource.containerWrite == 'openstack:*'" --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: SwiftContainer @@ -21,6 +22,7 @@ status: resource: name: swiftcontainer-update containerRead: ".r:*,.rlistings" + containerWrite: "openstack:*" conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml index ed3f61db0..44e8c802f 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml @@ -6,8 +6,9 @@ metadata: spec: resource: metadata: - - key: environment + - name: environment value: staging - - key: team + - name: team value: platform containerRead: ".r:*,.rlistings" + containerWrite: "openstack:*" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml deleted file mode 100644 index d1f225283..000000000 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-assert.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -apiVersion: openstack.k-orc.cloud/v1alpha1 -kind: SwiftContainer -metadata: - name: swiftcontainer-validation-valid -status: - conditions: - - type: Available - status: "True" - reason: Success - - type: Progressing - status: "False" - reason: Success diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml deleted file mode 100644 index f0fb63e85..000000000 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-secret.yaml +++ /dev/null @@ -1,5 +0,0 @@ -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/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml deleted file mode 100644 index d3890f4a4..000000000 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/00-valid-resource.yaml +++ /dev/null @@ -1,11 +0,0 @@ ---- -apiVersion: openstack.k-orc.cloud/v1alpha1 -kind: SwiftContainer -metadata: - name: swiftcontainer-validation-valid -spec: - cloudCredentialsRef: - cloudName: openstack - secretName: openstack-clouds - managementPolicy: managed - resource: {} diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml deleted file mode 100644 index 29d2d8029..000000000 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-assert.yaml +++ /dev/null @@ -1,23 +0,0 @@ ---- -# The valid container from step 00 must continue to be available. -apiVersion: openstack.k-orc.cloud/v1alpha1 -kind: SwiftContainer -metadata: - name: swiftcontainer-validation-valid -status: - conditions: - - type: Available - status: "True" - reason: Success - - type: Progressing - status: "False" - reason: Success ---- -apiVersion: kuttl.dev/v1beta1 -kind: TestAssert -commands: - # Verify that the invalid SwiftContainer does NOT exist in the cluster. - # The CRD pattern validation should have rejected the apply command in the - # previous step, so this object must be absent. - - script: "! kubectl get swiftcontainer swiftcontainer-validation-slash --namespace $NAMESPACE" - skipLogOutput: true diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml deleted file mode 100644 index d74920e1f..000000000 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/01-invalid-slash.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: kuttl.dev/v1beta1 -kind: TestStep -commands: - # Attempt to create a SwiftContainer with a forward slash in its name. The - # CRD pattern constraint (^[^/]+$) on SwiftContainerName rejects this at the - # Kubernetes API level, so the command is expected to fail. We set - # ignoreFailure: true so the test step does not abort; the assertion in - # 01-assert.yaml then verifies that the object was not created. - - command: >- - echo '{"apiVersion":"openstack.k-orc.cloud/v1alpha1","kind":"SwiftContainer","metadata":{"name":"swiftcontainer-validation-slash"},"spec":{"cloudCredentialsRef":{"cloudName":"openstack","secretName":"openstack-clouds"},"managementPolicy":"managed","resource":{"name":"invalid/slash/name"}}}' | kubectl apply -n ${NAMESPACE} -f - - namespaced: true - ignoreFailure: true diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md deleted file mode 100644 index 4df2a6bae..000000000 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-validation/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# SwiftContainer name validation - -## Step 00 - -Create a valid SwiftContainer to confirm the controller is operational, and -wait for it to become available. - -## Step 01 - -Attempt to create a SwiftContainer whose underlying OpenStack container name -contains a forward slash (`/`). The Swift object-storage specification forbids -forward slashes in container names because they are used as path separators in -the object-storage URL. - -The `SwiftContainerName` type enforces this constraint at two layers: - -1. **CRD-level validation**: The kubebuilder pattern constraint `^[^/]+$` on - the `SwiftContainerName` type rejects the object at the Kubernetes API - level. The `kubectl apply` command will fail with a validation error. - -2. **Controller-level validation** (defense-in-depth): Even if a name with a - forward slash somehow bypasses API-level validation (e.g., via a direct - etcd write), the controller detects the slash before calling the Swift API - and sets `Available=False` with reason `InvalidConfiguration` and a message - mentioning `forward slashes`. - -This test exercises layer 1: the invalid container creation is attempted with -`ignoreFailure: true` (because the API is expected to reject it), and the -assertion verifies that: -- The invalid container does **not** exist in the cluster (was rejected) -- The valid container from step 00 continues to be `Available=True` - -## Validation note - -The 256-byte name length limit is enforced only at the CRD level via a CEL -rule (`self.size() <= 256`). Testing this via KUTTL is impractical because: -- YAML/JSON strings of exactly 257 bytes are cumbersome to write in test files -- The validation is already exercised by the unit tests in `actuator_test.go` - -## Reference - -- Swift object-storage specification: container names must not contain `/` -- https://k-orc.cloud/development/writing-tests/ diff --git a/internal/osclients/swiftcontainer_test.go b/internal/osclients/swiftcontainer_test.go deleted file mode 100644 index 48259ade6..000000000 --- a/internal/osclients/swiftcontainer_test.go +++ /dev/null @@ -1,444 +0,0 @@ -/* -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 osclients_test - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gophercloud/gophercloud/v2" - "github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers" - "github.com/gophercloud/utils/v2/openstack/clientconfig" - - "github.com/k-orc/openstack-resource-controller/v2/internal/osclients" -) - -const fakeToken = "test-token-abc123" - -// newFakeSwiftServer creates an httptest server and a corresponding SwiftContainerClient -// pointing to it. The caller is responsible for closing the server. -func newFakeSwiftServer(t *testing.T, mux *http.ServeMux) (*httptest.Server, osclients.SwiftContainerClient) { - t.Helper() - - server := httptest.NewServer(mux) - - // Construct a gophercloud ServiceClient directly, pointing to the test server. - // This avoids needing real OpenStack credentials. - serviceClient := &gophercloud.ServiceClient{ - ProviderClient: &gophercloud.ProviderClient{ - TokenID: fakeToken, - }, - Endpoint: server.URL + "/", - } - - client := osclients.NewSwiftContainerClientFromServiceClient(serviceClient) - return server, client -} - -// TestNewSwiftContainerClient_Success verifies the constructor creates a valid client -// with a correct endpoint when given a valid provider. -func TestNewSwiftContainerClient_Success(t *testing.T) { - mux := http.NewServeMux() - server := httptest.NewServer(mux) - defer server.Close() - - // Provide a fake endpoint locator that returns the test server URL. - providerClient := &gophercloud.ProviderClient{ - TokenID: fakeToken, - EndpointLocator: func(eo gophercloud.EndpointOpts) (string, error) { - return server.URL + "/", nil - }, - } - - opts := &clientconfig.ClientOpts{} - client, err := osclients.NewSwiftContainerClient(providerClient, opts) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if client == nil { - t.Fatal("expected non-nil client") - } -} - -// TestNewSwiftContainerClient_InvalidEndpoint verifies that an error is returned -// when the endpoint locator returns an error (simulating an invalid or missing endpoint). -func TestNewSwiftContainerClient_InvalidEndpoint(t *testing.T) { - endpointErr := errors.New("endpoint not found") - - providerClient := &gophercloud.ProviderClient{ - TokenID: fakeToken, - EndpointLocator: func(eo gophercloud.EndpointOpts) (string, error) { - return "", endpointErr - }, - } - - opts := &clientconfig.ClientOpts{} - client, err := osclients.NewSwiftContainerClient(providerClient, opts) - if err == nil { - t.Fatal("expected error, got nil") - } - if client != nil { - t.Fatalf("expected nil client on error, got: %v", client) - } -} - -// TestListContainers_Pagination verifies that the iterator correctly yields containers -// across paginated responses. -func TestListContainers_Pagination(t *testing.T) { - mux := http.NewServeMux() - - // The Swift list containers API is paginated using the marker parameter. - // First page returns two containers; second (with marker) returns one; final returns empty. - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - t.Errorf("expected GET, got %s", r.Method) - } - if got := r.Header.Get("X-Auth-Token"); got != fakeToken { - t.Errorf("expected token %q, got %q", fakeToken, got) - } - - if err := r.ParseForm(); err != nil { - t.Errorf("failed to parse form: %v", err) - } - marker := r.Form.Get("marker") - - w.Header().Set("Content-Type", "application/json") - switch marker { - case "": - _, _ = fmt.Fprint(w, `[{"name":"container-a","count":0,"bytes":0},{"name":"container-b","count":3,"bytes":1024}]`) - case "container-b": - _, _ = fmt.Fprint(w, `[]`) - default: - t.Errorf("unexpected marker: %q", marker) - w.WriteHeader(http.StatusBadRequest) - } - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - var got []string - for container, err := range client.ListContainers(ctx, containers.ListOpts{}) { - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - got = append(got, container.Name) - } - - want := []string{"container-a", "container-b"} - if len(got) != len(want) { - t.Fatalf("expected %d containers, got %d: %v", len(want), len(got), got) - } - for i := range want { - if got[i] != want[i] { - t.Errorf("container[%d]: expected %q, got %q", i, want[i], got[i]) - } - } -} - -// TestListContainers_Error verifies that errors from page extraction are propagated -// through the iterator correctly. -// -// When a Swift server returns a 200 response with a non-JSON content type, the -// page body cannot be parsed as a container list, so ExtractInfo returns an error. -// The iterator must propagate this error to callers rather than silently stopping. -func TestListContainers_Error(t *testing.T) { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // Return 200 with text/plain - gophercloud accepts it as a valid page - // but ExtractInfo fails to unmarshal the body as []containers.Container, - // propagating the error through yieldPage. - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - _, _ = fmt.Fprint(w, "not-valid-json-container-data") - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - var gotErr error - for _, err := range client.ListContainers(ctx, containers.ListOpts{}) { - if err != nil { - gotErr = err - break - } - } - - if gotErr == nil { - t.Fatal("expected error from iterator, got nil") - } -} - -// TestCreateContainer_Success verifies that CreateContainer sends the correct -// headers, including ACLs and custom metadata. -func TestCreateContainer_Success(t *testing.T) { - const containerName = "my-container" - - mux := http.NewServeMux() - mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPut { - t.Errorf("expected PUT, got %s", r.Method) - } - - // Verify ACL headers. - if got := r.Header.Get("X-Container-Read"); got != ".r:*" { - t.Errorf("X-Container-Read: expected %q, got %q", ".r:*", got) - } - if got := r.Header.Get("X-Container-Write"); got != "myproject:myuser" { - t.Errorf("X-Container-Write: expected %q, got %q", "myproject:myuser", got) - } - - // Verify that custom metadata is sent with the correct prefix. - if got := r.Header.Get("X-Container-Meta-Environment"); got != "production" { - t.Errorf("X-Container-Meta-Environment: expected %q, got %q", "production", got) - } - - w.Header().Set("Content-Length", "0") - w.Header().Set("Content-Type", "text/html; charset=UTF-8") - w.Header().Set("X-Trans-Id", "tx-test-id") - w.WriteHeader(http.StatusNoContent) - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - opts := containers.CreateOpts{ - ContainerRead: ".r:*", - ContainerWrite: "myproject:myuser", - Metadata: map[string]string{ - "Environment": "production", - }, - } - header, err := client.CreateContainer(ctx, containerName, opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if header == nil { - t.Fatal("expected non-nil header") - } -} - -// TestGetContainer_Success verifies that GetContainer returns correct header information. -func TestGetContainer_Success(t *testing.T) { - const containerName = "my-container" - - mux := http.NewServeMux() - mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodHead { - t.Errorf("expected HEAD, got %s", r.Method) - } - - w.Header().Set("X-Container-Bytes-Used", "2048") - w.Header().Set("X-Container-Object-Count", "10") - w.Header().Set("X-Container-Read", ".r:*") - w.Header().Set("X-Container-Write", "admin") - w.Header().Set("X-Storage-Policy", "gold") - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("X-Trans-Id", "tx-get-test") - w.WriteHeader(http.StatusNoContent) - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - header, err := client.GetContainer(ctx, containerName, nil) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if header == nil { - t.Fatal("expected non-nil header") - } - if header.BytesUsed != 2048 { - t.Errorf("BytesUsed: expected 2048, got %d", header.BytesUsed) - } - if header.ObjectCount != 10 { - t.Errorf("ObjectCount: expected 10, got %d", header.ObjectCount) - } - if header.StoragePolicy != "gold" { - t.Errorf("StoragePolicy: expected %q, got %q", "gold", header.StoragePolicy) - } -} - -// TestGetContainerMetadata_Success verifies that metadata extraction works correctly, -// stripping the "X-Container-Meta-" prefix from header keys. -func TestGetContainerMetadata_Success(t *testing.T) { - const containerName = "my-container" - - mux := http.NewServeMux() - mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodHead { - t.Errorf("expected HEAD, got %s", r.Method) - } - - // Gophercloud's ExtractMetadata strips the "X-Container-Meta-" prefix - // and lowercases the key. - w.Header().Set("X-Container-Meta-Env", "staging") - w.Header().Set("X-Container-Meta-Owner", "team-infra") - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusNoContent) - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - meta, err := client.GetContainerMetadata(ctx, containerName) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if meta == nil { - t.Fatal("expected non-nil metadata map") - } - - // Gophercloud strips the "X-Container-Meta-" prefix. Go's net/http - // canonicalises header keys, so "X-Container-Meta-Env" becomes key "Env". - if got := meta["Env"]; got != "staging" { - t.Errorf("metadata[Env]: expected %q, got %q", "staging", got) - } - if got := meta["Owner"]; got != "team-infra" { - t.Errorf("metadata[Owner]: expected %q, got %q", "team-infra", got) - } -} - -// TestUpdateContainer_ACLs verifies that ACL updates are sent as the correct headers. -func TestUpdateContainer_ACLs(t *testing.T) { - const containerName = "my-container" - const newReadACL = ".r:*,.rlistings" - const newWriteACL = "myproject:*" - - mux := http.NewServeMux() - mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - t.Errorf("expected POST, got %s", r.Method) - } - - if got := r.Header.Get("X-Container-Read"); got != newReadACL { - t.Errorf("X-Container-Read: expected %q, got %q", newReadACL, got) - } - if got := r.Header.Get("X-Container-Write"); got != newWriteACL { - t.Errorf("X-Container-Write: expected %q, got %q", newWriteACL, got) - } - - w.Header().Set("Content-Length", "0") - w.Header().Set("Content-Type", "text/html; charset=UTF-8") - w.Header().Set("X-Trans-Id", "tx-update-test") - w.WriteHeader(http.StatusNoContent) - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - readACL := newReadACL - writeACL := newWriteACL - opts := containers.UpdateOpts{ - ContainerRead: &readACL, - ContainerWrite: &writeACL, - } - header, err := client.UpdateContainer(ctx, containerName, opts) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if header == nil { - t.Fatal("expected non-nil header") - } -} - -// TestDeleteContainer_Success verifies that the delete call succeeds and sends -// a DELETE request to the correct path. -func TestDeleteContainer_Success(t *testing.T) { - const containerName = "my-container" - - mux := http.NewServeMux() - mux.HandleFunc("/"+containerName, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { - t.Errorf("expected DELETE, got %s", r.Method) - } - w.WriteHeader(http.StatusNoContent) - }) - - server, client := newFakeSwiftServer(t, mux) - defer server.Close() - - ctx := context.Background() - err := client.DeleteContainer(ctx, containerName) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -// TestSwiftContainerErrorClient verifies that the error client returns the -// configured error for every method. -func TestSwiftContainerErrorClient(t *testing.T) { - testErr := errors.New("test configured error") - client := osclients.NewSwiftContainerErrorClient(testErr) - ctx := context.Background() - - t.Run("ListContainers", func(t *testing.T) { - var gotErr error - for _, err := range client.ListContainers(ctx, nil) { - gotErr = err - break - } - if !errors.Is(gotErr, testErr) { - t.Errorf("expected %v, got %v", testErr, gotErr) - } - }) - - t.Run("CreateContainer", func(t *testing.T) { - _, err := client.CreateContainer(ctx, "any", nil) - if !errors.Is(err, testErr) { - t.Errorf("expected %v, got %v", testErr, err) - } - }) - - t.Run("GetContainer", func(t *testing.T) { - _, err := client.GetContainer(ctx, "any", nil) - if !errors.Is(err, testErr) { - t.Errorf("expected %v, got %v", testErr, err) - } - }) - - t.Run("GetContainerMetadata", func(t *testing.T) { - _, err := client.GetContainerMetadata(ctx, "any") - if !errors.Is(err, testErr) { - t.Errorf("expected %v, got %v", testErr, err) - } - }) - - t.Run("DeleteContainer", func(t *testing.T) { - err := client.DeleteContainer(ctx, "any") - if !errors.Is(err, testErr) { - t.Errorf("expected %v, got %v", testErr, err) - } - }) - - t.Run("UpdateContainer", func(t *testing.T) { - _, err := client.UpdateContainer(ctx, "any", nil) - if !errors.Is(err, testErr) { - t.Errorf("expected %v, got %v", testErr, err) - } - }) -} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go index ea014f10f..a178c68b9 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go @@ -21,7 +21,7 @@ package v1alpha1 // SwiftContainerMetadataApplyConfiguration represents a declarative configuration of the SwiftContainerMetadata type for use // with apply. type SwiftContainerMetadataApplyConfiguration struct { - Key *string `json:"key,omitempty"` + Name *string `json:"name,omitempty"` Value *string `json:"value,omitempty"` } @@ -31,11 +31,11 @@ func SwiftContainerMetadata() *SwiftContainerMetadataApplyConfiguration { return &SwiftContainerMetadataApplyConfiguration{} } -// WithKey sets the Key field in the declarative configuration to the given value +// 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 Key field is set to the value of the last call. -func (b *SwiftContainerMetadataApplyConfiguration) WithKey(value string) *SwiftContainerMetadataApplyConfiguration { - b.Key = &value +// If called multiple times, the Name field is set to the value of the last call. +func (b *SwiftContainerMetadataApplyConfiguration) WithName(value string) *SwiftContainerMetadataApplyConfiguration { + b.Name = &value return b } diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go index 65f60268b..9d654e23f 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go @@ -21,7 +21,7 @@ package v1alpha1 // SwiftContainerMetadataStatusApplyConfiguration represents a declarative configuration of the SwiftContainerMetadataStatus type for use // with apply. type SwiftContainerMetadataStatusApplyConfiguration struct { - Key *string `json:"key,omitempty"` + Name *string `json:"name,omitempty"` Value *string `json:"value,omitempty"` } @@ -31,11 +31,11 @@ func SwiftContainerMetadataStatus() *SwiftContainerMetadataStatusApplyConfigurat return &SwiftContainerMetadataStatusApplyConfiguration{} } -// WithKey sets the Key field in the declarative configuration to the given value +// 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 Key field is set to the value of the last call. -func (b *SwiftContainerMetadataStatusApplyConfiguration) WithKey(value string) *SwiftContainerMetadataStatusApplyConfiguration { - b.Key = &value +// If called multiple times, the Name field is set to the value of the last call. +func (b *SwiftContainerMetadataStatusApplyConfiguration) WithName(value string) *SwiftContainerMetadataStatusApplyConfiguration { + b.Name = &value return b } diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 2f42dcd6f..05096e9e5 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -3573,7 +3573,7 @@ var schemaYAML = typed.YAMLObject(`types: - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadata map: fields: - - name: key + - name: name type: scalar: string - name: value @@ -3583,7 +3583,7 @@ var schemaYAML = typed.YAMLObject(`types: - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadataStatus map: fields: - - name: key + - name: name type: scalar: string - name: value @@ -3603,7 +3603,9 @@ var schemaYAML = typed.YAMLObject(`types: list: elementType: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadata - elementRelationship: atomic + elementRelationship: associative + keys: + - name - name: name type: scalar: string @@ -3627,7 +3629,9 @@ var schemaYAML = typed.YAMLObject(`types: list: elementType: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadataStatus - elementRelationship: atomic + elementRelationship: associative + keys: + - name - name: name type: scalar: string diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index cee30646d..3bfba9757 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -4677,7 +4677,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `key` _string_ | key is the name of the metadata item. It will be used as the suffix of
the X-Container-Meta-* header. | | MaxLength: 255
MinLength: 1
Required: \{\}
| +| `name` _string_ | name is the name of the metadata item. It will be used as the suffix of
the X-Container-Meta-* header. | | MaxLength: 255
MinLength: 1
Required: \{\}
| | `value` _string_ | value is the value of the metadata item. | | MaxLength: 255
Required: \{\}
| @@ -4695,7 +4695,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `key` _string_ | key is the name of the metadata item. | | MaxLength: 255
Optional: \{\}
| +| `name` _string_ | name is the name of the metadata item. | | MaxLength: 255
MinLength: 1
Required: \{\}
| | `value` _string_ | value is the value of the metadata item. | | MaxLength: 255
Optional: \{\}
| @@ -4733,8 +4733,8 @@ _Appears in:_ | --- | --- | --- | --- | | `name` _[SwiftContainerName](#swiftcontainername)_ | name will be the name of the created Swift container. If not specified,
the name of the ORC object will be used. The name must be unique within
the account and must not contain forward slashes. | | MaxLength: 256
MinLength: 1
Pattern: `^[^/]+$`
Optional: \{\}
| | `metadata` _[SwiftContainerMetadata](#swiftcontainermetadata) array_ | Refer to Kubernetes API documentation for fields of `metadata`. | | MaxItems: 64
Optional: \{\}
| -| `containerRead` _string_ | containerRead sets the X-Container-Read ACL header which defines who
can read objects in the container. Common values include ".r:*" for
public read access or a comma-separated list of account/container
combinations. | | MaxLength: 256
Optional: \{\}
| -| `containerWrite` _string_ | containerWrite sets the X-Container-Write ACL header which defines who
can write objects to the container. Common values include a
comma-separated list of account/container combinations. | | MaxLength: 256
Optional: \{\}
| +| `containerRead` _string_ | containerRead sets the X-Container-Read ACL header which defines who
can read objects in the container. Common values include ".r:*" for
public read access or a comma-separated list of account/container
combinations. | | MaxLength: 256
MinLength: 1
Optional: \{\}
| +| `containerWrite` _string_ | containerWrite sets the X-Container-Write ACL header which defines who
can write objects to the container. Common values include a
comma-separated list of account/container combinations. | | MaxLength: 256
MinLength: 1
Optional: \{\}
| | `storagePolicy` _string_ | storagePolicy is the name of the storage policy to use for this
container. If not specified, the cluster's default storage policy will
be used. This field is immutable after creation. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| From 8d8ca53443fd46fdaf161bf3dd176edc4580e440 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Mon, 29 Jun 2026 10:14:08 +0300 Subject: [PATCH 6/7] Tighten SwiftContainer API and e2e assertions --- api/v1alpha1/swiftcontainer_types.go | 10 ---------- api/v1alpha1/zz_generated.deepcopy.go | 10 ---------- cmd/models-schema/zz_generated.openapi.go | 14 -------------- .../openstack.k-orc.cloud_swiftcontainers.yaml | 12 ------------ .../swiftcontainer-create-full/00-assert.yaml | 6 +++--- .../00-create-resource.yaml | 1 + .../tests/swiftcontainer-create-full/README.md | 2 ++ .../00-assert.yaml | 2 ++ .../tests/swiftcontainer-import/02-assert.yaml | 2 ++ .../tests/swiftcontainer-update/00-assert.yaml | 2 ++ .../tests/swiftcontainer-update/01-assert.yaml | 2 ++ .../tests/swiftcontainer-update/02-assert.yaml | 2 ++ .../tests/swiftcontainer-update/README.md | 4 ++-- .../v1alpha1/swiftcontainerresourcestatus.go | 18 ------------------ .../applyconfiguration/internal/internal.go | 6 ------ website/docs/crd-reference.md | 2 -- 16 files changed, 18 insertions(+), 77 deletions(-) diff --git a/api/v1alpha1/swiftcontainer_types.go b/api/v1alpha1/swiftcontainer_types.go index 5a68c2535..f628e785f 100644 --- a/api/v1alpha1/swiftcontainer_types.go +++ b/api/v1alpha1/swiftcontainer_types.go @@ -178,14 +178,4 @@ type SwiftContainerResourceStatus struct { // +kubebuilder:validation:MaxLength=1024 // +optional Versions string `json:"versions,omitempty"` - - // quotaBytes is the quota on the maximum number of bytes that can be - // stored in the container, if set. - // +optional - QuotaBytes *int64 `json:"quotaBytes,omitempty"` - - // quotaCount is the quota on the maximum number of objects that can be - // stored in the container, if set. - // +optional - QuotaCount *int64 `json:"quotaCount,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e3b6f19a2..3c399bee7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6355,16 +6355,6 @@ func (in *SwiftContainerResourceStatus) DeepCopyInto(out *SwiftContainerResource *out = make([]SwiftContainerMetadataStatus, len(*in)) copy(*out, *in) } - if in.QuotaBytes != nil { - in, out := &in.QuotaBytes, &out.QuotaBytes - *out = new(int64) - **out = **in - } - if in.QuotaCount != nil { - in, out := &in.QuotaCount, &out.QuotaCount - *out = new(int64) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SwiftContainerResourceStatus. diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index cf898280c..60051c95a 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -12110,20 +12110,6 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource Format: "", }, }, - "quotaBytes": { - SchemaProps: spec.SchemaProps{ - Description: "quotaBytes is the quota on the maximum number of bytes that can be stored in the container, if set.", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "quotaCount": { - SchemaProps: spec.SchemaProps{ - Description: "quotaCount is the quota on the maximum number of objects that can be stored in the container, if set.", - Type: []string{"integer"}, - Format: "int64", - }, - }, }, }, }, diff --git a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml index 863988496..0357d071f 100644 --- a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml @@ -385,18 +385,6 @@ spec: container. format: int64 type: integer - quotaBytes: - description: |- - quotaBytes is the quota on the maximum number of bytes that can be - stored in the container, if set. - format: int64 - type: integer - quotaCount: - description: |- - quotaCount is the quota on the maximum number of objects that can be - stored in the container, if set. - format: int64 - type: integer storagePolicy: description: storagePolicy is the name of the storage policy assigned to the container. diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml index 78aeb2427..9098dd544 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml @@ -6,8 +6,11 @@ metadata: status: resource: name: swiftcontainer-create-full-override + bytesUsed: 0 + objectCount: 0 containerRead: ".r:*,.rlistings" containerWrite: "openstack:*" + storagePolicy: Policy-0 conditions: - type: Available status: "True" @@ -26,9 +29,6 @@ resourceRefs: assertAll: - celExpr: "swiftcontainer.status.id != ''" - celExpr: "swiftcontainer.status.id == 'swiftcontainer-create-full-override'" - - celExpr: "swiftcontainer.status.resource.name == 'swiftcontainer-create-full-override'" - - celExpr: "swiftcontainer.status.resource.containerRead == '.r:*,.rlistings'" - - celExpr: "swiftcontainer.status.resource.containerWrite == 'openstack:*'" - celExpr: "has(swiftcontainer.status.resource.metadata)" - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Environment' && m.value == 'test')" - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Owner' && m.value == 'orc-e2e')" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml index 627dc45b6..bbbb16e36 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml @@ -17,3 +17,4 @@ spec: value: orc-e2e containerRead: ".r:*,.rlistings" containerWrite: "openstack:*" + storagePolicy: Policy-0 diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md index 2c40e0230..9ab312501 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/README.md @@ -12,6 +12,8 @@ Validates that: `status.resource.metadata`. - Read ACL (`containerRead`) and write ACL (`containerWrite`) are configured and reflected in `status.resource`. +- Storage policy (`storagePolicy`) is configured and reflected in + `status.resource`. - `Available=True` and `Progressing=False` conditions are set. ## Reference diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml index 4b433ebf8..83bc8c14f 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-minimal/00-assert.yaml @@ -6,6 +6,8 @@ metadata: status: resource: name: swiftcontainer-create-minimal + bytesUsed: 0 + objectCount: 0 conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml index e8832b15f..c1a2c533f 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-import/02-assert.yaml @@ -34,3 +34,5 @@ status: reason: Success resource: name: swiftcontainer-import-external + bytesUsed: 0 + objectCount: 0 diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml index 428e3098c..52885a915 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/00-assert.yaml @@ -19,6 +19,8 @@ metadata: status: resource: name: swiftcontainer-update + bytesUsed: 0 + objectCount: 0 conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml index 285ca7121..75adad1c4 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml @@ -21,6 +21,8 @@ metadata: status: resource: name: swiftcontainer-update + bytesUsed: 0 + objectCount: 0 containerRead: ".r:*,.rlistings" containerWrite: "openstack:*" conditions: diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml index 428e3098c..52885a915 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/02-assert.yaml @@ -19,6 +19,8 @@ metadata: status: resource: name: swiftcontainer-update + bytesUsed: 0 + objectCount: 0 conditions: - type: Available status: "True" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md index ee8fd899c..d982fa124 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/README.md @@ -9,7 +9,7 @@ and verify that the observed state corresponds to the spec. Update all mutable fields: - Add custom metadata key-value pairs -- Set a read ACL (`containerRead`) +- Set read and write ACLs (`containerRead` and `containerWrite`) Verify that all updated properties are reflected in the resource status. @@ -19,7 +19,7 @@ Revert the resource to its original value (no metadata, no ACLs) and verify the resulting object matches the initial creation state. Validates that: -- Clearing `containerRead` (by removing the field) removes the ACL from the container. +- Clearing `containerRead` and `containerWrite` (by removing the fields) removes the ACLs from the container. - Removing metadata entries removes them from the container. - `Available=True` and `Progressing=False` conditions are set after each step. diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go index 2f89f36b0..9a0ccdc1f 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainerresourcestatus.go @@ -29,8 +29,6 @@ type SwiftContainerResourceStatusApplyConfiguration struct { ContainerWrite *string `json:"containerWrite,omitempty"` StoragePolicy *string `json:"storagePolicy,omitempty"` Versions *string `json:"versions,omitempty"` - QuotaBytes *int64 `json:"quotaBytes,omitempty"` - QuotaCount *int64 `json:"quotaCount,omitempty"` } // SwiftContainerResourceStatusApplyConfiguration constructs a declarative configuration of the SwiftContainerResourceStatus type for use with @@ -107,19 +105,3 @@ func (b *SwiftContainerResourceStatusApplyConfiguration) WithVersions(value stri b.Versions = &value return b } - -// WithQuotaBytes sets the QuotaBytes 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 QuotaBytes field is set to the value of the last call. -func (b *SwiftContainerResourceStatusApplyConfiguration) WithQuotaBytes(value int64) *SwiftContainerResourceStatusApplyConfiguration { - b.QuotaBytes = &value - return b -} - -// WithQuotaCount sets the QuotaCount 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 QuotaCount field is set to the value of the last call. -func (b *SwiftContainerResourceStatusApplyConfiguration) WithQuotaCount(value int64) *SwiftContainerResourceStatusApplyConfiguration { - b.QuotaCount = &value - return b -} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 05096e9e5..c34866dac 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -3638,12 +3638,6 @@ var schemaYAML = typed.YAMLObject(`types: - name: objectCount type: scalar: numeric - - name: quotaBytes - type: - scalar: numeric - - name: quotaCount - type: - scalar: numeric - name: storagePolicy type: scalar: string diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 3bfba9757..37b6a67ce 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -4759,8 +4759,6 @@ _Appears in:_ | `containerWrite` _string_ | containerWrite is the current X-Container-Write ACL, defining who can
write objects to the container. | | MaxLength: 1024
Optional: \{\}
| | `storagePolicy` _string_ | storagePolicy is the name of the storage policy assigned to the container. | | MaxLength: 1024
Optional: \{\}
| | `versions` _string_ | versions is the container where object versions are stored, if versioning
is enabled on this container. | | MaxLength: 1024
Optional: \{\}
| -| `quotaBytes` _integer_ | quotaBytes is the quota on the maximum number of bytes that can be
stored in the container, if set. | | Optional: \{\}
| -| `quotaCount` _integer_ | quotaCount is the quota on the maximum number of objects that can be
stored in the container, if set. | | Optional: \{\}
| #### SwiftContainerSpec From 222add3bbb18ae53267d6770825f86fa06d8a92b Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Mon, 29 Jun 2026 10:34:37 +0300 Subject: [PATCH 7/7] Use key for SwiftContainer metadata entries --- api/v1alpha1/swiftcontainer_types.go | 12 ++++++------ cmd/models-schema/zz_generated.openapi.go | 16 ++++++++-------- .../openstack.k-orc.cloud_swiftcontainers.yaml | 16 ++++++++-------- examples/swiftcontainer/full.yaml | 4 ++-- internal/controllers/swiftcontainer/actuator.go | 4 ++-- .../controllers/swiftcontainer/actuator_test.go | 8 ++++---- internal/controllers/swiftcontainer/status.go | 2 +- .../swiftcontainer-create-full/00-assert.yaml | 4 ++-- .../00-create-resource.yaml | 4 ++-- .../tests/swiftcontainer-update/01-assert.yaml | 4 ++-- .../01-updated-resource.yaml | 4 ++-- .../api/v1alpha1/swiftcontainermetadata.go | 10 +++++----- .../api/v1alpha1/swiftcontainermetadatastatus.go | 10 +++++----- .../applyconfiguration/internal/internal.go | 8 ++++---- website/docs/crd-reference.md | 4 ++-- 15 files changed, 55 insertions(+), 55 deletions(-) diff --git a/api/v1alpha1/swiftcontainer_types.go b/api/v1alpha1/swiftcontainer_types.go index f628e785f..fc6648bc9 100644 --- a/api/v1alpha1/swiftcontainer_types.go +++ b/api/v1alpha1/swiftcontainer_types.go @@ -27,12 +27,12 @@ type SwiftContainerName string // SwiftContainerMetadata defines a key-value pair to be set as a Swift // container metadata header (X-Container-Meta-: ). type SwiftContainerMetadata struct { - // name is the name of the metadata item. It will be used as the suffix of + // key is the key of the metadata item. It will be used as the suffix of // the X-Container-Meta-* header. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=255 // +required - Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` // value is the value of the metadata item. // +kubebuilder:validation:MaxLength:=255 @@ -43,11 +43,11 @@ type SwiftContainerMetadata struct { // SwiftContainerMetadataStatus represents an observed metadata key-value pair // on a Swift container. type SwiftContainerMetadataStatus struct { - // name is the name of the metadata item. + // key is the key of the metadata item. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=255 // +required - Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` // value is the value of the metadata item. // +kubebuilder:validation:MaxLength:=255 @@ -103,7 +103,7 @@ type SwiftContainerResourceSpec struct { // X-Container-Meta-* headers on the Swift container. // +kubebuilder:validation:MaxItems:=64 // +listType=map - // +listMapKey=name + // +listMapKey=key // +optional Metadata []SwiftContainerMetadata `json:"metadata,omitempty"` @@ -152,7 +152,7 @@ type SwiftContainerResourceStatus struct { // metadata is the list of observed metadata key-value pairs on the container. // +kubebuilder:validation:MaxItems:=64 // +listType=map - // +listMapKey=name + // +listMapKey=key // +optional Metadata []SwiftContainerMetadataStatus `json:"metadata,omitempty"` diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 60051c95a..af82c9031 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -11917,9 +11917,9 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata Description: "SwiftContainerMetadata defines a key-value pair to be set as a Swift container metadata header (X-Container-Meta-: ).", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "name": { + "key": { SchemaProps: spec.SchemaProps{ - Description: "name is the name of the metadata item. It will be used as the suffix of the X-Container-Meta-* header.", + Description: "key is the key of the metadata item. It will be used as the suffix of the X-Container-Meta-* header.", Type: []string{"string"}, Format: "", }, @@ -11933,7 +11933,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata }, }, }, - Required: []string{"name", "value"}, + Required: []string{"key", "value"}, }, }, } @@ -11946,9 +11946,9 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata Description: "SwiftContainerMetadataStatus represents an observed metadata key-value pair on a Swift container.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "name": { + "key": { SchemaProps: spec.SchemaProps{ - Description: "name is the name of the metadata item.", + Description: "key is the key of the metadata item.", Type: []string{"string"}, Format: "", }, @@ -11961,7 +11961,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerMetadata }, }, }, - Required: []string{"name"}, + Required: []string{"key"}, }, }, } @@ -11985,7 +11985,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ "x-kubernetes-list-map-keys": []interface{}{ - "name", + "key", }, "x-kubernetes-list-type": "map", }, @@ -12064,7 +12064,7 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_SwiftContainerResource VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ "x-kubernetes-list-map-keys": []interface{}{ - "name", + "key", }, "x-kubernetes-list-type": "map", }, diff --git a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml index 0357d071f..431dab334 100644 --- a/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_swiftcontainers.yaml @@ -185,9 +185,9 @@ spec: SwiftContainerMetadata defines a key-value pair to be set as a Swift container metadata header (X-Container-Meta-: ). properties: - name: + key: description: |- - name is the name of the metadata item. It will be used as the suffix of + key is the key of the metadata item. It will be used as the suffix of the X-Container-Meta-* header. maxLength: 255 minLength: 1 @@ -197,13 +197,13 @@ spec: maxLength: 255 type: string required: - - name + - key - value type: object maxItems: 64 type: array x-kubernetes-list-map-keys: - - name + - key x-kubernetes-list-type: map name: description: |- @@ -359,8 +359,8 @@ spec: SwiftContainerMetadataStatus represents an observed metadata key-value pair on a Swift container. properties: - name: - description: name is the name of the metadata item. + key: + description: key is the key of the metadata item. maxLength: 255 minLength: 1 type: string @@ -369,12 +369,12 @@ spec: maxLength: 255 type: string required: - - name + - key type: object maxItems: 64 type: array x-kubernetes-list-map-keys: - - name + - key x-kubernetes-list-type: map name: description: name is the name of the Swift container. diff --git a/examples/swiftcontainer/full.yaml b/examples/swiftcontainer/full.yaml index 15830ebd8..2645e8726 100644 --- a/examples/swiftcontainer/full.yaml +++ b/examples/swiftcontainer/full.yaml @@ -12,9 +12,9 @@ spec: name: my-custom-bucket # metadata sets arbitrary key-value pairs as X-Container-Meta-* headers. metadata: - - name: owner + - key: owner value: myteam - - name: environment + - key: environment value: production # containerRead sets the X-Container-Read ACL. Use ".r:*" for public read access. containerRead: .r:*,.rlistings diff --git a/internal/controllers/swiftcontainer/actuator.go b/internal/controllers/swiftcontainer/actuator.go index f2fd169b8..e302cf77a 100644 --- a/internal/controllers/swiftcontainer/actuator.go +++ b/internal/controllers/swiftcontainer/actuator.go @@ -209,7 +209,7 @@ func (actuator swiftcontainerActuator) CreateResource(ctx context.Context, obj o if len(resource.Metadata) > 0 { metadata := make(map[string]string, len(resource.Metadata)) for _, m := range resource.Metadata { - metadata[m.Name] = m.Value + metadata[m.Key] = m.Value } createOpts.Metadata = metadata } @@ -322,7 +322,7 @@ func (actuator swiftcontainerActuator) reconcileMetadata(ctx context.Context, or // in Swift). desiredMetadata := make(map[string]string, len(resource.Metadata)) for _, m := range resource.Metadata { - desiredMetadata[strings.ToLower(m.Name)] = m.Value + desiredMetadata[strings.ToLower(m.Key)] = m.Value } // Find keys to add/update and keys to remove. diff --git a/internal/controllers/swiftcontainer/actuator_test.go b/internal/controllers/swiftcontainer/actuator_test.go index 1ed7b844b..1f0b200db 100644 --- a/internal/controllers/swiftcontainer/actuator_test.go +++ b/internal/controllers/swiftcontainer/actuator_test.go @@ -396,8 +396,8 @@ func TestListOSResourcesForAdoption(t *testing.T) { actuator := swiftcontainerActuator{client} orcObject := newSwiftContainerObject("my-object", &orcv1alpha1.SwiftContainerResourceSpec{ Metadata: []orcv1alpha1.SwiftContainerMetadata{ - {Name: "env", Value: "prod"}, - {Name: "team", Value: "infra"}, + {Key: "env", Value: "prod"}, + {Key: "team", Value: "infra"}, }, }) @@ -471,8 +471,8 @@ func TestCreateResource(t *testing.T) { orcObject := newSwiftContainerObject("full-container", &orcv1alpha1.SwiftContainerResourceSpec{ Name: ptr.To[orcv1alpha1.SwiftContainerName]("full-container"), Metadata: []orcv1alpha1.SwiftContainerMetadata{ - {Name: "project", Value: "orc"}, - {Name: "env", Value: "test"}, + {Key: "project", Value: "orc"}, + {Key: "env", Value: "test"}, }, ContainerRead: ".r:*", ContainerWrite: "account:user", diff --git a/internal/controllers/swiftcontainer/status.go b/internal/controllers/swiftcontainer/status.go index becdff975..1436144a4 100644 --- a/internal/controllers/swiftcontainer/status.go +++ b/internal/controllers/swiftcontainer/status.go @@ -88,7 +88,7 @@ func (swiftcontainerStatusWriter) ApplyResourceStatus(_ logr.Logger, osResource for _, k := range keys { resourceStatus.WithMetadata( orcapplyconfigv1alpha1.SwiftContainerMetadataStatus(). - WithName(k). + WithKey(k). WithValue(osResource.Metadata[k]), ) } diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml index 9098dd544..412672e1f 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-assert.yaml @@ -30,5 +30,5 @@ assertAll: - celExpr: "swiftcontainer.status.id != ''" - celExpr: "swiftcontainer.status.id == 'swiftcontainer-create-full-override'" - celExpr: "has(swiftcontainer.status.resource.metadata)" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Environment' && m.value == 'test')" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Owner' && m.value == 'orc-e2e')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Environment' && m.value == 'test')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Owner' && m.value == 'orc-e2e')" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml index bbbb16e36..90219cd98 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-create-full/00-create-resource.yaml @@ -11,9 +11,9 @@ spec: resource: name: swiftcontainer-create-full-override metadata: - - name: environment + - key: environment value: test - - name: owner + - key: owner value: orc-e2e containerRead: ".r:*,.rlistings" containerWrite: "openstack:*" diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml index 75adad1c4..6d8904875 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-assert.yaml @@ -9,8 +9,8 @@ resourceRefs: assertAll: - celExpr: "swiftcontainer.status.id == 'swiftcontainer-update'" - celExpr: "has(swiftcontainer.status.resource.metadata)" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Environment' && m.value == 'staging')" - - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.name == 'Team' && m.value == 'platform')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Environment' && m.value == 'staging')" + - celExpr: "swiftcontainer.status.resource.metadata.exists(m, m.key == 'Team' && m.value == 'platform')" - celExpr: "swiftcontainer.status.resource.containerRead == '.r:*,.rlistings'" - celExpr: "swiftcontainer.status.resource.containerWrite == 'openstack:*'" --- diff --git a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml index 44e8c802f..0447e9476 100644 --- a/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml +++ b/internal/controllers/swiftcontainer/tests/swiftcontainer-update/01-updated-resource.yaml @@ -6,9 +6,9 @@ metadata: spec: resource: metadata: - - name: environment + - key: environment value: staging - - name: team + - key: team value: platform containerRead: ".r:*,.rlistings" containerWrite: "openstack:*" diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go index a178c68b9..ea014f10f 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadata.go @@ -21,7 +21,7 @@ package v1alpha1 // SwiftContainerMetadataApplyConfiguration represents a declarative configuration of the SwiftContainerMetadata type for use // with apply. type SwiftContainerMetadataApplyConfiguration struct { - Name *string `json:"name,omitempty"` + Key *string `json:"key,omitempty"` Value *string `json:"value,omitempty"` } @@ -31,11 +31,11 @@ func SwiftContainerMetadata() *SwiftContainerMetadataApplyConfiguration { return &SwiftContainerMetadataApplyConfiguration{} } -// WithName sets the Name field in the declarative configuration to the given value +// WithKey sets the Key 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 *SwiftContainerMetadataApplyConfiguration) WithName(value string) *SwiftContainerMetadataApplyConfiguration { - b.Name = &value +// If called multiple times, the Key field is set to the value of the last call. +func (b *SwiftContainerMetadataApplyConfiguration) WithKey(value string) *SwiftContainerMetadataApplyConfiguration { + b.Key = &value return b } diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go index 9d654e23f..65f60268b 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/swiftcontainermetadatastatus.go @@ -21,7 +21,7 @@ package v1alpha1 // SwiftContainerMetadataStatusApplyConfiguration represents a declarative configuration of the SwiftContainerMetadataStatus type for use // with apply. type SwiftContainerMetadataStatusApplyConfiguration struct { - Name *string `json:"name,omitempty"` + Key *string `json:"key,omitempty"` Value *string `json:"value,omitempty"` } @@ -31,11 +31,11 @@ func SwiftContainerMetadataStatus() *SwiftContainerMetadataStatusApplyConfigurat return &SwiftContainerMetadataStatusApplyConfiguration{} } -// WithName sets the Name field in the declarative configuration to the given value +// WithKey sets the Key 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 *SwiftContainerMetadataStatusApplyConfiguration) WithName(value string) *SwiftContainerMetadataStatusApplyConfiguration { - b.Name = &value +// If called multiple times, the Key field is set to the value of the last call. +func (b *SwiftContainerMetadataStatusApplyConfiguration) WithKey(value string) *SwiftContainerMetadataStatusApplyConfiguration { + b.Key = &value return b } diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index c34866dac..c1c12b3bd 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -3573,7 +3573,7 @@ var schemaYAML = typed.YAMLObject(`types: - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadata map: fields: - - name: name + - name: key type: scalar: string - name: value @@ -3583,7 +3583,7 @@ var schemaYAML = typed.YAMLObject(`types: - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadataStatus map: fields: - - name: name + - name: key type: scalar: string - name: value @@ -3605,7 +3605,7 @@ var schemaYAML = typed.YAMLObject(`types: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadata elementRelationship: associative keys: - - name + - key - name: name type: scalar: string @@ -3631,7 +3631,7 @@ var schemaYAML = typed.YAMLObject(`types: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.SwiftContainerMetadataStatus elementRelationship: associative keys: - - name + - key - name: name type: scalar: string diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 37b6a67ce..405ce8a9e 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -4677,7 +4677,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `name` _string_ | name is the name of the metadata item. It will be used as the suffix of
the X-Container-Meta-* header. | | MaxLength: 255
MinLength: 1
Required: \{\}
| +| `key` _string_ | key is the key of the metadata item. It will be used as the suffix of
the X-Container-Meta-* header. | | MaxLength: 255
MinLength: 1
Required: \{\}
| | `value` _string_ | value is the value of the metadata item. | | MaxLength: 255
Required: \{\}
| @@ -4695,7 +4695,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `name` _string_ | name is the name of the metadata item. | | MaxLength: 255
MinLength: 1
Required: \{\}
| +| `key` _string_ | key is the key of the metadata item. | | MaxLength: 255
MinLength: 1
Required: \{\}
| | `value` _string_ | value is the value of the metadata item. | | MaxLength: 255
Optional: \{\}
|