diff --git a/apis/cluster/compute/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/compute/v1alpha1/zz_generated.deepcopy.go index 29f54d6..4ff02e7 100644 --- a/apis/cluster/compute/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cluster/compute/v1alpha1/zz_generated.deepcopy.go @@ -319,6 +319,21 @@ func (in *BlockDeviceInitParameters) DeepCopyInto(out *BlockDeviceInitParameters *out = new(string) **out = **in } + if in.ImageName != nil { + in, out := &in.ImageName, &out.ImageName + *out = new(string) + **out = **in + } + if in.ImageNameRef != nil { + in, out := &in.ImageNameRef, &out.ImageNameRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.ImageNameSelector != nil { + in, out := &in.ImageNameSelector, &out.ImageNameSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } if in.Multiattach != nil { in, out := &in.Multiattach, &out.Multiattach *out = new(bool) @@ -399,6 +414,11 @@ func (in *BlockDeviceObservation) DeepCopyInto(out *BlockDeviceObservation) { *out = new(string) **out = **in } + if in.ImageName != nil { + in, out := &in.ImageName, &out.ImageName + *out = new(string) + **out = **in + } if in.Multiattach != nil { in, out := &in.Multiattach, &out.Multiattach *out = new(bool) @@ -469,6 +489,21 @@ func (in *BlockDeviceParameters) DeepCopyInto(out *BlockDeviceParameters) { *out = new(string) **out = **in } + if in.ImageName != nil { + in, out := &in.ImageName, &out.ImageName + *out = new(string) + **out = **in + } + if in.ImageNameRef != nil { + in, out := &in.ImageNameRef, &out.ImageNameRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.ImageNameSelector != nil { + in, out := &in.ImageNameSelector, &out.ImageNameSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } if in.Multiattach != nil { in, out := &in.Multiattach, &out.Multiattach *out = new(bool) diff --git a/apis/cluster/compute/v1alpha1/zz_generated.resolvers.go b/apis/cluster/compute/v1alpha1/zz_generated.resolvers.go index 5390778..ab067aa 100644 --- a/apis/cluster/compute/v1alpha1/zz_generated.resolvers.go +++ b/apis/cluster/compute/v1alpha1/zz_generated.resolvers.go @@ -10,9 +10,10 @@ package v1alpha1 import ( "context" - v1alpha11 "github.com/crossplane-contrib/provider-openstack/apis/cluster/blockstorage/v1alpha1" + v1alpha12 "github.com/crossplane-contrib/provider-openstack/apis/cluster/blockstorage/v1alpha1" v1alpha1 "github.com/crossplane-contrib/provider-openstack/apis/cluster/identity/v1alpha1" - v1alpha12 "github.com/crossplane-contrib/provider-openstack/apis/cluster/networking/v1alpha1" + v1alpha11 "github.com/crossplane-contrib/provider-openstack/apis/cluster/images/v1alpha1" + v1alpha13 "github.com/crossplane-contrib/provider-openstack/apis/cluster/networking/v1alpha1" reference "github.com/crossplane/crossplane-runtime/v2/pkg/reference" resource "github.com/crossplane/upjet/v2/pkg/resource" errors "github.com/pkg/errors" @@ -105,6 +106,25 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er var mrsp reference.MultiResolutionResponse var err error + for i3 := 0; i3 < len(mg.Spec.ForProvider.BlockDevice); i3++ { + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.BlockDevice[i3].ImageName), + Extract: resource.ExtractParamPath("name", false), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.ForProvider.BlockDevice[i3].ImageNameRef, + Selector: mg.Spec.ForProvider.BlockDevice[i3].ImageNameSelector, + To: reference.To{ + List: &v1alpha11.ImageV2List{}, + Managed: &v1alpha11.ImageV2{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.BlockDevice[i3].ImageName") + } + mg.Spec.ForProvider.BlockDevice[i3].ImageName = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.BlockDevice[i3].ImageNameRef = rsp.ResolvedReference + + } for i3 := 0; i3 < len(mg.Spec.ForProvider.BlockDevice); i3++ { rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.BlockDevice[i3].UUID), @@ -113,8 +133,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.ForProvider.BlockDevice[i3].UUIDRef, Selector: mg.Spec.ForProvider.BlockDevice[i3].UUIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { @@ -166,8 +186,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.ForProvider.Network[i3].PortRef, Selector: mg.Spec.ForProvider.Network[i3].PortSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -184,8 +204,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er References: mg.Spec.ForProvider.SecurityGroupsRefs, Selector: mg.Spec.ForProvider.SecurityGroupsSelector, To: reference.To{ - List: &v1alpha12.SecgroupV2List{}, - Managed: &v1alpha12.SecgroupV2{}, + List: &v1alpha13.SecgroupV2List{}, + Managed: &v1alpha13.SecgroupV2{}, }, }) if err != nil { @@ -194,6 +214,25 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er mg.Spec.ForProvider.SecurityGroups = reference.ToPtrValues(mrsp.ResolvedValues) mg.Spec.ForProvider.SecurityGroupsRefs = mrsp.ResolvedReferences + for i3 := 0; i3 < len(mg.Spec.InitProvider.BlockDevice); i3++ { + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.InitProvider.BlockDevice[i3].ImageName), + Extract: resource.ExtractParamPath("name", false), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.InitProvider.BlockDevice[i3].ImageNameRef, + Selector: mg.Spec.InitProvider.BlockDevice[i3].ImageNameSelector, + To: reference.To{ + List: &v1alpha11.ImageV2List{}, + Managed: &v1alpha11.ImageV2{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.InitProvider.BlockDevice[i3].ImageName") + } + mg.Spec.InitProvider.BlockDevice[i3].ImageName = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.InitProvider.BlockDevice[i3].ImageNameRef = rsp.ResolvedReference + + } for i3 := 0; i3 < len(mg.Spec.InitProvider.BlockDevice); i3++ { rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ CurrentValue: reference.FromPtrValue(mg.Spec.InitProvider.BlockDevice[i3].UUID), @@ -202,8 +241,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.InitProvider.BlockDevice[i3].UUIDRef, Selector: mg.Spec.InitProvider.BlockDevice[i3].UUIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { @@ -255,8 +294,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.InitProvider.Network[i3].PortRef, Selector: mg.Spec.InitProvider.Network[i3].PortSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -273,8 +312,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er References: mg.Spec.InitProvider.SecurityGroupsRefs, Selector: mg.Spec.InitProvider.SecurityGroupsSelector, To: reference.To{ - List: &v1alpha12.SecgroupV2List{}, - Managed: &v1alpha12.SecgroupV2{}, + List: &v1alpha13.SecgroupV2List{}, + Managed: &v1alpha13.SecgroupV2{}, }, }) if err != nil { @@ -317,8 +356,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.ForProvider.NetworkIDRef, Selector: mg.Spec.ForProvider.NetworkIDSelector, To: reference.To{ - List: &v1alpha12.NetworkV2List{}, - Managed: &v1alpha12.NetworkV2{}, + List: &v1alpha13.NetworkV2List{}, + Managed: &v1alpha13.NetworkV2{}, }, }) if err != nil { @@ -334,8 +373,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.ForProvider.PortIDRef, Selector: mg.Spec.ForProvider.PortIDSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -368,8 +407,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.InitProvider.NetworkIDRef, Selector: mg.Spec.InitProvider.NetworkIDSelector, To: reference.To{ - List: &v1alpha12.NetworkV2List{}, - Managed: &v1alpha12.NetworkV2{}, + List: &v1alpha13.NetworkV2List{}, + Managed: &v1alpha13.NetworkV2{}, }, }) if err != nil { @@ -385,8 +424,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.InitProvider.PortIDRef, Selector: mg.Spec.InitProvider.PortIDSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -473,8 +512,8 @@ func (mg *VolumeAttachV2) ResolveReferences(ctx context.Context, c client.Reader Reference: mg.Spec.ForProvider.VolumeIDRef, Selector: mg.Spec.ForProvider.VolumeIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { @@ -507,8 +546,8 @@ func (mg *VolumeAttachV2) ResolveReferences(ctx context.Context, c client.Reader Reference: mg.Spec.InitProvider.VolumeIDRef, Selector: mg.Spec.InitProvider.VolumeIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { diff --git a/apis/cluster/compute/v1alpha1/zz_instancev2_terraformed.go b/apis/cluster/compute/v1alpha1/zz_instancev2_terraformed.go index 6af926f..bbd65bb 100755 --- a/apis/cluster/compute/v1alpha1/zz_instancev2_terraformed.go +++ b/apis/cluster/compute/v1alpha1/zz_instancev2_terraformed.go @@ -121,6 +121,7 @@ func (tr *InstanceV2) LateInitialize(attrs []byte) (bool, error) { return false, errors.Wrap(err, "failed to unmarshal Terraform state parameters for late-initialization") } opts := []resource.GenericLateInitializerOption{resource.WithZeroValueJSONOmitEmptyFilter(resource.CNameWildcard)} + opts = append(opts, resource.WithNameFilter("BlockDevice.ImageName")) opts = append(opts, resource.WithNameFilter("FlavorID")) opts = append(opts, resource.WithNameFilter("FlavorName")) diff --git a/apis/cluster/compute/v1alpha1/zz_instancev2_types.go b/apis/cluster/compute/v1alpha1/zz_instancev2_types.go index 2debfb1..0b52ed4 100755 --- a/apis/cluster/compute/v1alpha1/zz_instancev2_types.go +++ b/apis/cluster/compute/v1alpha1/zz_instancev2_types.go @@ -47,6 +47,21 @@ type BlockDeviceInitParameters struct { // creates a new server. GuestFormat *string `json:"guestFormat,omitempty" tf:"guest_format,omitempty"` + // The name of the + // desired image for the server. Changing this rebuilds the existing server. + // In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + // +crossplane:generate:reference:type=github.com/crossplane-contrib/provider-openstack/apis/cluster/images/v1alpha1.ImageV2 + // +crossplane:generate:reference:extractor=github.com/crossplane/upjet/v2/pkg/resource.ExtractParamPath("name",false) + ImageName *string `json:"imageName,omitempty" tf:"image_name,omitempty"` + + // Reference to a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameRef *v1.Reference `json:"imageNameRef,omitempty" tf:"-"` + + // Selector for a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameSelector *v1.Selector `json:"imageNameSelector,omitempty" tf:"-"` + // Enable the attachment of multiattach-capable // volumes. Multiattach *bool `json:"multiattach,omitempty" tf:"multiattach,omitempty"` @@ -114,6 +129,11 @@ type BlockDeviceObservation struct { // creates a new server. GuestFormat *string `json:"guestFormat,omitempty" tf:"guest_format,omitempty"` + // The name of the + // desired image for the server. Changing this rebuilds the existing server. + // In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + ImageName *string `json:"imageName,omitempty" tf:"image_name,omitempty"` + // Enable the attachment of multiattach-capable // volumes. Multiattach *bool `json:"multiattach,omitempty" tf:"multiattach,omitempty"` @@ -177,6 +197,22 @@ type BlockDeviceParameters struct { // +kubebuilder:validation:Optional GuestFormat *string `json:"guestFormat,omitempty" tf:"guest_format,omitempty"` + // The name of the + // desired image for the server. Changing this rebuilds the existing server. + // In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + // +crossplane:generate:reference:type=github.com/crossplane-contrib/provider-openstack/apis/cluster/images/v1alpha1.ImageV2 + // +crossplane:generate:reference:extractor=github.com/crossplane/upjet/v2/pkg/resource.ExtractParamPath("name",false) + // +kubebuilder:validation:Optional + ImageName *string `json:"imageName,omitempty" tf:"image_name,omitempty"` + + // Reference to a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameRef *v1.Reference `json:"imageNameRef,omitempty" tf:"-"` + + // Selector for a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameSelector *v1.Selector `json:"imageNameSelector,omitempty" tf:"-"` + // Enable the attachment of multiattach-capable // volumes. // +kubebuilder:validation:Optional diff --git a/apis/namespaced/compute/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/compute/v1alpha1/zz_generated.deepcopy.go index d4512b1..0bee4e0 100644 --- a/apis/namespaced/compute/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/compute/v1alpha1/zz_generated.deepcopy.go @@ -319,6 +319,21 @@ func (in *BlockDeviceInitParameters) DeepCopyInto(out *BlockDeviceInitParameters *out = new(string) **out = **in } + if in.ImageName != nil { + in, out := &in.ImageName, &out.ImageName + *out = new(string) + **out = **in + } + if in.ImageNameRef != nil { + in, out := &in.ImageNameRef, &out.ImageNameRef + *out = new(v1.NamespacedReference) + (*in).DeepCopyInto(*out) + } + if in.ImageNameSelector != nil { + in, out := &in.ImageNameSelector, &out.ImageNameSelector + *out = new(v1.NamespacedSelector) + (*in).DeepCopyInto(*out) + } if in.Multiattach != nil { in, out := &in.Multiattach, &out.Multiattach *out = new(bool) @@ -399,6 +414,11 @@ func (in *BlockDeviceObservation) DeepCopyInto(out *BlockDeviceObservation) { *out = new(string) **out = **in } + if in.ImageName != nil { + in, out := &in.ImageName, &out.ImageName + *out = new(string) + **out = **in + } if in.Multiattach != nil { in, out := &in.Multiattach, &out.Multiattach *out = new(bool) @@ -469,6 +489,21 @@ func (in *BlockDeviceParameters) DeepCopyInto(out *BlockDeviceParameters) { *out = new(string) **out = **in } + if in.ImageName != nil { + in, out := &in.ImageName, &out.ImageName + *out = new(string) + **out = **in + } + if in.ImageNameRef != nil { + in, out := &in.ImageNameRef, &out.ImageNameRef + *out = new(v1.NamespacedReference) + (*in).DeepCopyInto(*out) + } + if in.ImageNameSelector != nil { + in, out := &in.ImageNameSelector, &out.ImageNameSelector + *out = new(v1.NamespacedSelector) + (*in).DeepCopyInto(*out) + } if in.Multiattach != nil { in, out := &in.Multiattach, &out.Multiattach *out = new(bool) diff --git a/apis/namespaced/compute/v1alpha1/zz_generated.resolvers.go b/apis/namespaced/compute/v1alpha1/zz_generated.resolvers.go index 737caa3..993b921 100644 --- a/apis/namespaced/compute/v1alpha1/zz_generated.resolvers.go +++ b/apis/namespaced/compute/v1alpha1/zz_generated.resolvers.go @@ -10,9 +10,10 @@ package v1alpha1 import ( "context" - v1alpha11 "github.com/crossplane-contrib/provider-openstack/apis/namespaced/blockstorage/v1alpha1" + v1alpha12 "github.com/crossplane-contrib/provider-openstack/apis/namespaced/blockstorage/v1alpha1" v1alpha1 "github.com/crossplane-contrib/provider-openstack/apis/namespaced/identity/v1alpha1" - v1alpha12 "github.com/crossplane-contrib/provider-openstack/apis/namespaced/networking/v1alpha1" + v1alpha11 "github.com/crossplane-contrib/provider-openstack/apis/namespaced/images/v1alpha1" + v1alpha13 "github.com/crossplane-contrib/provider-openstack/apis/namespaced/networking/v1alpha1" reference "github.com/crossplane/crossplane-runtime/v2/pkg/reference" resource "github.com/crossplane/upjet/v2/pkg/resource" errors "github.com/pkg/errors" @@ -105,6 +106,25 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er var mrsp reference.MultiNamespacedResolutionResponse var err error + for i3 := 0; i3 < len(mg.Spec.ForProvider.BlockDevice); i3++ { + rsp, err = r.Resolve(ctx, reference.NamespacedResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.BlockDevice[i3].ImageName), + Extract: resource.ExtractParamPath("name", false), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.ForProvider.BlockDevice[i3].ImageNameRef, + Selector: mg.Spec.ForProvider.BlockDevice[i3].ImageNameSelector, + To: reference.To{ + List: &v1alpha11.ImageV2List{}, + Managed: &v1alpha11.ImageV2{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.BlockDevice[i3].ImageName") + } + mg.Spec.ForProvider.BlockDevice[i3].ImageName = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.BlockDevice[i3].ImageNameRef = rsp.ResolvedReference + + } for i3 := 0; i3 < len(mg.Spec.ForProvider.BlockDevice); i3++ { rsp, err = r.Resolve(ctx, reference.NamespacedResolutionRequest{ CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.BlockDevice[i3].UUID), @@ -113,8 +133,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.ForProvider.BlockDevice[i3].UUIDRef, Selector: mg.Spec.ForProvider.BlockDevice[i3].UUIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { @@ -166,8 +186,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.ForProvider.Network[i3].PortRef, Selector: mg.Spec.ForProvider.Network[i3].PortSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -184,8 +204,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er References: mg.Spec.ForProvider.SecurityGroupsRefs, Selector: mg.Spec.ForProvider.SecurityGroupsSelector, To: reference.To{ - List: &v1alpha12.SecgroupV2List{}, - Managed: &v1alpha12.SecgroupV2{}, + List: &v1alpha13.SecgroupV2List{}, + Managed: &v1alpha13.SecgroupV2{}, }, }) if err != nil { @@ -194,6 +214,25 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er mg.Spec.ForProvider.SecurityGroups = reference.ToPtrValues(mrsp.ResolvedValues) mg.Spec.ForProvider.SecurityGroupsRefs = mrsp.ResolvedReferences + for i3 := 0; i3 < len(mg.Spec.InitProvider.BlockDevice); i3++ { + rsp, err = r.Resolve(ctx, reference.NamespacedResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.InitProvider.BlockDevice[i3].ImageName), + Extract: resource.ExtractParamPath("name", false), + Namespace: mg.GetNamespace(), + Reference: mg.Spec.InitProvider.BlockDevice[i3].ImageNameRef, + Selector: mg.Spec.InitProvider.BlockDevice[i3].ImageNameSelector, + To: reference.To{ + List: &v1alpha11.ImageV2List{}, + Managed: &v1alpha11.ImageV2{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.InitProvider.BlockDevice[i3].ImageName") + } + mg.Spec.InitProvider.BlockDevice[i3].ImageName = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.InitProvider.BlockDevice[i3].ImageNameRef = rsp.ResolvedReference + + } for i3 := 0; i3 < len(mg.Spec.InitProvider.BlockDevice); i3++ { rsp, err = r.Resolve(ctx, reference.NamespacedResolutionRequest{ CurrentValue: reference.FromPtrValue(mg.Spec.InitProvider.BlockDevice[i3].UUID), @@ -202,8 +241,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.InitProvider.BlockDevice[i3].UUIDRef, Selector: mg.Spec.InitProvider.BlockDevice[i3].UUIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { @@ -255,8 +294,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er Reference: mg.Spec.InitProvider.Network[i3].PortRef, Selector: mg.Spec.InitProvider.Network[i3].PortSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -273,8 +312,8 @@ func (mg *InstanceV2) ResolveReferences(ctx context.Context, c client.Reader) er References: mg.Spec.InitProvider.SecurityGroupsRefs, Selector: mg.Spec.InitProvider.SecurityGroupsSelector, To: reference.To{ - List: &v1alpha12.SecgroupV2List{}, - Managed: &v1alpha12.SecgroupV2{}, + List: &v1alpha13.SecgroupV2List{}, + Managed: &v1alpha13.SecgroupV2{}, }, }) if err != nil { @@ -317,8 +356,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.ForProvider.NetworkIDRef, Selector: mg.Spec.ForProvider.NetworkIDSelector, To: reference.To{ - List: &v1alpha12.NetworkV2List{}, - Managed: &v1alpha12.NetworkV2{}, + List: &v1alpha13.NetworkV2List{}, + Managed: &v1alpha13.NetworkV2{}, }, }) if err != nil { @@ -334,8 +373,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.ForProvider.PortIDRef, Selector: mg.Spec.ForProvider.PortIDSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -368,8 +407,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.InitProvider.NetworkIDRef, Selector: mg.Spec.InitProvider.NetworkIDSelector, To: reference.To{ - List: &v1alpha12.NetworkV2List{}, - Managed: &v1alpha12.NetworkV2{}, + List: &v1alpha13.NetworkV2List{}, + Managed: &v1alpha13.NetworkV2{}, }, }) if err != nil { @@ -385,8 +424,8 @@ func (mg *InterfaceAttachV2) ResolveReferences(ctx context.Context, c client.Rea Reference: mg.Spec.InitProvider.PortIDRef, Selector: mg.Spec.InitProvider.PortIDSelector, To: reference.To{ - List: &v1alpha12.PortV2List{}, - Managed: &v1alpha12.PortV2{}, + List: &v1alpha13.PortV2List{}, + Managed: &v1alpha13.PortV2{}, }, }) if err != nil { @@ -473,8 +512,8 @@ func (mg *VolumeAttachV2) ResolveReferences(ctx context.Context, c client.Reader Reference: mg.Spec.ForProvider.VolumeIDRef, Selector: mg.Spec.ForProvider.VolumeIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { @@ -507,8 +546,8 @@ func (mg *VolumeAttachV2) ResolveReferences(ctx context.Context, c client.Reader Reference: mg.Spec.InitProvider.VolumeIDRef, Selector: mg.Spec.InitProvider.VolumeIDSelector, To: reference.To{ - List: &v1alpha11.VolumeV3List{}, - Managed: &v1alpha11.VolumeV3{}, + List: &v1alpha12.VolumeV3List{}, + Managed: &v1alpha12.VolumeV3{}, }, }) if err != nil { diff --git a/apis/namespaced/compute/v1alpha1/zz_instancev2_terraformed.go b/apis/namespaced/compute/v1alpha1/zz_instancev2_terraformed.go index 6af926f..bbd65bb 100755 --- a/apis/namespaced/compute/v1alpha1/zz_instancev2_terraformed.go +++ b/apis/namespaced/compute/v1alpha1/zz_instancev2_terraformed.go @@ -121,6 +121,7 @@ func (tr *InstanceV2) LateInitialize(attrs []byte) (bool, error) { return false, errors.Wrap(err, "failed to unmarshal Terraform state parameters for late-initialization") } opts := []resource.GenericLateInitializerOption{resource.WithZeroValueJSONOmitEmptyFilter(resource.CNameWildcard)} + opts = append(opts, resource.WithNameFilter("BlockDevice.ImageName")) opts = append(opts, resource.WithNameFilter("FlavorID")) opts = append(opts, resource.WithNameFilter("FlavorName")) diff --git a/apis/namespaced/compute/v1alpha1/zz_instancev2_types.go b/apis/namespaced/compute/v1alpha1/zz_instancev2_types.go index b422b4e..ba11d8d 100755 --- a/apis/namespaced/compute/v1alpha1/zz_instancev2_types.go +++ b/apis/namespaced/compute/v1alpha1/zz_instancev2_types.go @@ -48,6 +48,21 @@ type BlockDeviceInitParameters struct { // creates a new server. GuestFormat *string `json:"guestFormat,omitempty" tf:"guest_format,omitempty"` + // The name of the + // desired image for the server. Changing this rebuilds the existing server. + // In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + // +crossplane:generate:reference:type=github.com/crossplane-contrib/provider-openstack/apis/namespaced/images/v1alpha1.ImageV2 + // +crossplane:generate:reference:extractor=github.com/crossplane/upjet/v2/pkg/resource.ExtractParamPath("name",false) + ImageName *string `json:"imageName,omitempty" tf:"image_name,omitempty"` + + // Reference to a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameRef *v1.NamespacedReference `json:"imageNameRef,omitempty" tf:"-"` + + // Selector for a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameSelector *v1.NamespacedSelector `json:"imageNameSelector,omitempty" tf:"-"` + // Enable the attachment of multiattach-capable // volumes. Multiattach *bool `json:"multiattach,omitempty" tf:"multiattach,omitempty"` @@ -115,6 +130,11 @@ type BlockDeviceObservation struct { // creates a new server. GuestFormat *string `json:"guestFormat,omitempty" tf:"guest_format,omitempty"` + // The name of the + // desired image for the server. Changing this rebuilds the existing server. + // In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + ImageName *string `json:"imageName,omitempty" tf:"image_name,omitempty"` + // Enable the attachment of multiattach-capable // volumes. Multiattach *bool `json:"multiattach,omitempty" tf:"multiattach,omitempty"` @@ -178,6 +198,22 @@ type BlockDeviceParameters struct { // +kubebuilder:validation:Optional GuestFormat *string `json:"guestFormat,omitempty" tf:"guest_format,omitempty"` + // The name of the + // desired image for the server. Changing this rebuilds the existing server. + // In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + // +crossplane:generate:reference:type=github.com/crossplane-contrib/provider-openstack/apis/namespaced/images/v1alpha1.ImageV2 + // +crossplane:generate:reference:extractor=github.com/crossplane/upjet/v2/pkg/resource.ExtractParamPath("name",false) + // +kubebuilder:validation:Optional + ImageName *string `json:"imageName,omitempty" tf:"image_name,omitempty"` + + // Reference to a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameRef *v1.NamespacedReference `json:"imageNameRef,omitempty" tf:"-"` + + // Selector for a ImageV2 in images to populate imageName. + // +kubebuilder:validation:Optional + ImageNameSelector *v1.NamespacedSelector `json:"imageNameSelector,omitempty" tf:"-"` + // Enable the attachment of multiattach-capable // volumes. // +kubebuilder:validation:Optional diff --git a/config/cluster/compute/config.go b/config/cluster/compute/config.go index e430d8a..e0745af 100644 --- a/config/cluster/compute/config.go +++ b/config/cluster/compute/config.go @@ -1,6 +1,10 @@ package compute -import "github.com/crossplane/upjet/v2/pkg/config" +import ( + "github.com/crossplane/upjet/v2/pkg/config" + + "github.com/crossplane-contrib/provider-openstack/config/common/blockdevice" +) // Configure configures individual resources by adding custom ResourceConfigurators. func Configure(p *config.Provider) { @@ -23,6 +27,14 @@ func Configure(p *config.Provider) { r.References["security_groups"] = config.Reference{ TerraformName: "openstack_networking_secgroup_v2", } + // Allow block devices to use an image name instead of an image ID. + // The name is resolved to an ID by the provider at create time, so no + // ImageV2 object is required on the cluster (the reference is optional). + blockdevice.ConfigureImageName(r) + r.References["block_device.image_name"] = config.Reference{ + TerraformName: "openstack_images_image_v2", + Extractor: `github.com/crossplane/upjet/v2/pkg/resource.ExtractParamPath("name",false)`, + } }) p.AddResourceConfigurator("openstack_compute_quotaset_v2", func(r *config.Resource) { r.References["project_id"] = config.Reference{ diff --git a/config/common/blockdevice/imagename.go b/config/common/blockdevice/imagename.go new file mode 100644 index 0000000..d07c7ef --- /dev/null +++ b/config/common/blockdevice/imagename.go @@ -0,0 +1,300 @@ +// Package blockdevice extends the openstack_compute_instance_v2 Terraform +// schema so that block devices can reference an image by name. +// +// The upstream Terraform provider only accepts an image UUID in +// block_device.uuid (the top-level image_name is ignored for boot-from-volume). +// That makes it impossible to use the same manifest on clusters that do not +// have a matching ImageV2 object or the same image UUID. +// +// The extension is done fully on the Crossplane provider side: a new +// block_device.image_name attribute is added to the schema and resolved to +// block_device.uuid right before the upstream create function is called. +// On refresh the attribute is populated back from the image the boot volume +// was created from, so changing the name in the spec is detected as drift and +// forces a new server. +package blockdevice + +import ( + "context" + "fmt" + "log" + "strconv" + "strings" + + "github.com/crossplane/upjet/v2/pkg/config" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" + imagesutils "github.com/gophercloud/utils/v2/openstack/image/v2/images" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/pkg/errors" + "github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack" +) + +const ( + blockDeviceField = "block_device" + imageNameField = "image_name" + uuidField = "uuid" + sourceTypeField = "source_type" + + sourceTypeImage = "image" +) + +// imageResolver resolves an image name to an image ID. +type imageResolver func(ctx context.Context, d *schema.ResourceData, meta any, name string) (string, error) + +// imageNamer resolves an image ID to an image name. +type imageNamer func(ctx context.Context, d *schema.ResourceData, meta any, id string) (string, error) + +// ConfigureImageName adds block_device.image_name to the given resource and +// makes the create function resolve it to block_device.uuid. +func ConfigureImageName(r *config.Resource) { + if r.TerraformResource == nil { + return + } + extendResource(r.TerraformResource, resolveImageIDByName, resolveImageNameByID) + + // image_name is populated from the observed image on refresh. Late + // initialization must not copy it into spec next to uuid. + // + // uuid is intentionally NOT in IgnoredFields: upjet must late-initialize it + // for volume/snapshot devices that never use imageName. For image-backed + // devices with imageName in initProvider, late-init will copy the resolved + // UUID into forProvider, but wrapCreate handles the recreation case by + // resolving imageName fresh and overwriting any stale UUID. + r.LateInitializer.IgnoredFields = append(r.LateInitializer.IgnoredFields, blockDeviceField+"."+imageNameField) +} + +func extendResource(res *schema.Resource, resolve imageResolver, name imageNamer) { + bd, ok := res.Schema[blockDeviceField] + if !ok { + return + } + bdRes, ok := bd.Elem.(*schema.Resource) + if !ok { + return + } + + bdRes.Schema[imageNameField] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + // provider-metadata.yaml is regenerated by the scraper and has no entry + // for this attribute, so the description has to live here. Note that + // upjet prepends the top-level image_name docs as a fallback. + Description: "In a block device the name is resolved to the image ID before the server " + + "is created, so the image does not need to exist as a managed resource. " + + "Can only be used with source_type \"image\" and conflicts with uuid. " + + "Changing this creates a new server.", + DiffSuppressFunc: suppressImageName, + } + if uuid, ok := bdRes.Schema[uuidField]; ok { + uuid.DiffSuppressFunc = suppressUUID + } + + // CreateContext is nil when the schema is built from the JSON schema for + // code generation; there is nothing to wrap in that case. + if create := res.CreateContext; create != nil { + res.CreateContext = wrapCreate(create, resolve) + } + if read := res.ReadContext; read != nil { + res.ReadContext = wrapRead(read, name) + } +} + +// suppressImageName hides image_name diffs that cannot be compared: +// - the config does not set image_name (uuid is used instead), while the +// state holds the observed image name; +// - the existing server has no observed image name (e.g. the image was +// deleted or the lookup failed), so there is nothing to compare with. +// +// Otherwise a changed name is a real diff and forces a new server. +func suppressImageName(_, oldValue, newValue string, d *schema.ResourceData) bool { + if newValue == "" { + return true + } + + return d.Id() != "" && oldValue == "" +} + +// suppressUUID hides uuid diffs when the config sets image_name. uuid is then +// derived from the name: the state holds the resolved image ID, while the spec +// may hold nothing or a stale ID copied by late initialization before the image +// was replaced under the same name. Drift is detected on image_name instead. +func suppressUUID(k, _, _ string, d *schema.ResourceData) bool { + return configImageName(k, d) != "" +} + +// configImageName returns image_name of the block device owning key k as set +// in the configuration. ResourceData.Get falls back to the state for +// attributes missing from the configuration, so the raw config is read. +func configImageName(k string, d *schema.ResourceData) string { + parts := strings.Split(k, ".") + if len(parts) != 3 { + return "" + } + i, err := strconv.Atoi(parts[1]) + if err != nil { + return "" + } + v, diags := d.GetRawConfigAt(cty.GetAttrPath(blockDeviceField).IndexInt(i).GetAttr(imageNameField)) + if diags.HasError() || !v.IsKnown() || v.IsNull() || !v.Type().Equals(cty.String) { + return "" + } + + return v.AsString() +} + +func wrapCreate(create schema.CreateContextFunc, resolve imageResolver) schema.CreateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if err := resolveImageNames(ctx, d, meta, resolve); err != nil { + return diag.FromErr(err) + } + + return create(ctx, d, meta) + } +} + +// resolveImageNames replaces image_name with the resolved uuid in every block +// device that sets it. +func resolveImageNames(ctx context.Context, d *schema.ResourceData, meta any, resolve imageResolver) error { + devices, ok := d.Get(blockDeviceField).([]any) + if !ok { + return nil + } + + changed := false + for i, raw := range devices { + bd, ok := raw.(map[string]any) + if !ok { + continue + } + name, _ := bd[imageNameField].(string) + if name == "" { + continue + } + if uuid, _ := bd[uuidField].(string); uuid != "" { + // uuid may have been late-initialized from the observed TF state + // while imageName is set in initProvider. image_name takes + // precedence; the stale uuid is cleared so the upstream create does + // not receive conflicting inputs. + log.Printf("[DEBUG] block_device.%d: image_name set together with uuid %q"+ + " (likely late-initialized); clearing uuid and using image_name", i, uuid) + delete(bd, uuidField) + } + if st, _ := bd[sourceTypeField].(string); st != sourceTypeImage { + return fmt.Errorf("block_device.%d: image_name requires source_type %q, got %q", i, sourceTypeImage, st) + } + + id, err := resolve(ctx, d, meta, name) + if err != nil { + return errors.Wrapf(err, "block_device.%d: cannot resolve image_name %q", i, name) + } + bd[uuidField] = id + changed = true + } + + if !changed { + return nil + } + + return errors.Wrap(d.Set(blockDeviceField, devices), "cannot set resolved block_device") +} + +func resolveImageIDByName(ctx context.Context, d *schema.ResourceData, meta any, name string) (string, error) { + cfg, ok := meta.(*openstack.Config) + if !ok { + return "", errors.Errorf("unexpected provider meta type %T", meta) + } + client, err := cfg.ImageV2Client(ctx, openstack.GetRegion(d, cfg)) + if err != nil { + return "", errors.Wrap(err, "cannot create OpenStack image client") + } + + return imagesutils.IDFromName(ctx, client, name) +} + +func wrapRead(read schema.ReadContextFunc, name imageNamer) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + diags := read(ctx, d, meta) + if diags.HasError() || d.Id() == "" { + return diags + } + populateImageNames(ctx, d, meta, name) + + return diags + } +} + +// populateImageNames sets image_name of image-backed block devices to the +// current name of the image referenced by uuid. The upstream read function +// rebuilds block_device without it. Lookup failures are ignored and leave the +// previous value untouched, so a deleted or inaccessible image never causes a +// diff. +func populateImageNames(ctx context.Context, d *schema.ResourceData, meta any, name imageNamer) { + devices, ok := d.Get(blockDeviceField).([]any) + if !ok { + return + } + + cache := map[string]string{} + changed := false + for _, raw := range devices { + bd, ok := raw.(map[string]any) + if !ok { + continue + } + if setImageName(ctx, d, meta, name, cache, bd) { + changed = true + } + } + + if !changed { + return + } + if err := d.Set(blockDeviceField, devices); err != nil { + log.Printf("[DEBUG] cannot set block_device image names: %s", err) + } +} + +// setImageName fills image_name of a single image-backed block device and +// reports whether it was modified. Results are cached per image ID. +func setImageName(ctx context.Context, d *schema.ResourceData, meta any, name imageNamer, cache map[string]string, bd map[string]any) bool { + uuid, _ := bd[uuidField].(string) + if st, _ := bd[sourceTypeField].(string); st != sourceTypeImage || uuid == "" { + return false + } + + n, cached := cache[uuid] + if !cached { + var err error + if n, err = name(ctx, d, meta, uuid); err != nil { + log.Printf("[DEBUG] cannot resolve name of image %s: %s", uuid, err) + n = "" + } + cache[uuid] = n + } + if n == "" { + return false + } + bd[imageNameField] = n + + return true +} + +func resolveImageNameByID(ctx context.Context, d *schema.ResourceData, meta any, id string) (string, error) { + cfg, ok := meta.(*openstack.Config) + if !ok { + return "", errors.Errorf("unexpected provider meta type %T", meta) + } + client, err := cfg.ImageV2Client(ctx, openstack.GetRegion(d, cfg)) + if err != nil { + return "", errors.Wrap(err, "cannot create OpenStack image client") + } + img, err := images.Get(ctx, client, id).Extract() + if err != nil { + return "", err + } + + return img.Name, nil +} diff --git a/config/common/blockdevice/imagename_test.go b/config/common/blockdevice/imagename_test.go new file mode 100644 index 0000000..37d275c --- /dev/null +++ b/config/common/blockdevice/imagename_test.go @@ -0,0 +1,374 @@ +package blockdevice + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/pkg/errors" +) + +func testResource(create schema.CreateContextFunc, resolve imageResolver) *schema.Resource { + return testResourceWithRead(create, nil, resolve, nil) +} + +func testResourceWithRead(create schema.CreateContextFunc, read schema.ReadContextFunc, resolve imageResolver, name imageNamer) *schema.Resource { + res := &schema.Resource{ + CreateContext: create, + ReadContext: read, + Schema: map[string]*schema.Schema{ + "block_device": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source_type": {Type: schema.TypeString, Required: true, ForceNew: true}, + "uuid": {Type: schema.TypeString, Optional: true, ForceNew: true}, + }, + }, + }, + }, + } + extendResource(res, resolve, name) + + return res +} + +func fakeResolver(ids map[string]string) imageResolver { + return func(_ context.Context, _ *schema.ResourceData, _ any, name string) (string, error) { + id, ok := ids[name] + if !ok { + return "", errors.Errorf("image %q not found", name) + } + + return id, nil + } +} + +func TestExtendSchema(t *testing.T) { + res := testResource(nil, nil) + bd := res.Schema["block_device"].Elem.(*schema.Resource) + + in, ok := bd.Schema["image_name"] + if !ok { + t.Fatal("image_name was not added to block_device schema") + } + if !in.Optional || !in.ForceNew || in.Type != schema.TypeString { + t.Errorf("unexpected image_name schema: %+v", in) + } + if bd.Schema["uuid"].DiffSuppressFunc == nil { + t.Error("uuid DiffSuppressFunc was not set") + } + if res.CreateContext != nil || res.ReadContext != nil { + t.Error("nil CreateContext/ReadContext must stay nil") + } +} + +func TestCreateResolvesImageName(t *testing.T) { + var gotUUID, gotName string + create := func(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { + gotUUID = d.Get("block_device.0.uuid").(string) + gotName = d.Get("block_device.0.image_name").(string) + d.SetId("id") + + return nil + } + res := testResource(create, fakeResolver(map[string]string{"ubuntu": "1111"})) + + d := schema.TestResourceDataRaw(t, res.Schema, map[string]any{ + "block_device": []any{map[string]any{ + "source_type": "image", + "image_name": "ubuntu", + }}, + }) + if diags := res.CreateContext(context.Background(), d, nil); diags.HasError() { + t.Fatalf("unexpected error: %v", diags) + } + if gotUUID != "1111" { + t.Errorf("uuid = %q, want 1111", gotUUID) + } + if gotName != "ubuntu" { + t.Errorf("image_name = %q, want ubuntu", gotName) + } +} + +func TestCreateDoesNotTouchUUID(t *testing.T) { + called := false + create := func(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { + called = d.Get("block_device.0.uuid").(string) == "explicit" + + return nil + } + res := testResource(create, fakeResolver(nil)) + + d := schema.TestResourceDataRaw(t, res.Schema, map[string]any{ + "block_device": []any{map[string]any{ + "source_type": "image", + "uuid": "explicit", + }}, + }) + if diags := res.CreateContext(context.Background(), d, nil); diags.HasError() { + t.Fatalf("unexpected error: %v", diags) + } + if !called { + t.Error("explicit uuid was not preserved") + } +} + +func TestCreateValidation(t *testing.T) { + cases := map[string]map[string]any{ + "WrongSource": {"source_type": "volume", "image_name": "ubuntu"}, + "NotFound": {"source_type": "image", "image_name": "missing"}, + } + for name, bd := range cases { + t.Run(name, func(t *testing.T) { + create := func(context.Context, *schema.ResourceData, any) diag.Diagnostics { + t.Error("upstream create must not be called on validation error") + + return nil + } + res := testResource(create, fakeResolver(map[string]string{"ubuntu": "1111"})) + d := schema.TestResourceDataRaw(t, res.Schema, map[string]any{"block_device": []any{bd}}) + if diags := res.CreateContext(context.Background(), d, nil); !diags.HasError() { + t.Error("expected error") + } + }) + } +} + +// TestCreateImageNameWinsOverLateInitUUID covers the recreation scenario where +// imageName is set in initProvider and uuid was late-initialized into forProvider +// from the observed TF state. On recreation the merged params contain both fields. +// image_name must take precedence: uuid is cleared, imageName is resolved, and +// the resolved UUID is written back to ResourceData so late-init can pick it up +// on the next reconciliation. +func TestCreateImageNameWinsOverLateInitUUID(t *testing.T) { + var gotUUID string + create := func(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { + gotUUID = d.Get("block_device.0.uuid").(string) + d.SetId("new-id") + + return nil + } + res := testResource(create, fakeResolver(map[string]string{"ubuntu": "1111"})) + + // Simulate merged params: initProvider.imageName + forProvider.uuid (late-initialized). + d := schema.TestResourceDataRaw(t, res.Schema, map[string]any{ + "block_device": []any{map[string]any{ + "source_type": "image", + "image_name": "ubuntu", + "uuid": "stale-late-initialized-uuid", + }}, + }) + if diags := res.CreateContext(context.Background(), d, nil); diags.HasError() { + t.Fatalf("expected success when both set (late-init scenario): %v", diags) + } + // Resolver returns "1111" for "ubuntu"; the stale uuid must be replaced + // inside the upstream create call. + if gotUUID != "1111" { + t.Errorf("uuid inside create = %q, want 1111 (resolved from image_name)", gotUUID) + } + // The resolved UUID must also be written back into ResourceData so that + // upjet's late-initialization can copy it into spec.forProvider on the next + // reconciliation. Without this, the next diff would see a UUID mismatch and + // require instance replacement. + if got := d.Get("block_device.0.uuid"); got != "1111" { + t.Errorf("uuid in ResourceData after create = %v, want 1111 (for late-init)", got) + } +} + +// TestCreateImageNameWinsWhenImageReplaced covers the scenario where an image +// is replaced (same name, new UUID). forProvider.uuid holds the old UUID from a +// previous late-initialization. wrapCreate must use the freshly resolved UUID, +// and ResourceData must be updated so the next reconciliation sees no drift. +func TestCreateImageNameWinsWhenImageReplaced(t *testing.T) { + const oldUUID = "old-uuid-1111" + const newUUID = "new-uuid-2222" + + var gotUUID string + create := func(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { + gotUUID = d.Get("block_device.0.uuid").(string) + d.SetId("new-id") + + return nil + } + // Image "ubuntu" now resolves to the new UUID. + res := testResource(create, fakeResolver(map[string]string{"ubuntu": newUUID})) + + // forProvider.uuid = oldUUID (from previous late-init), imageName still set. + d := schema.TestResourceDataRaw(t, res.Schema, map[string]any{ + "block_device": []any{map[string]any{ + "source_type": "image", + "image_name": "ubuntu", + "uuid": oldUUID, + }}, + }) + if diags := res.CreateContext(context.Background(), d, nil); diags.HasError() { + t.Fatalf("unexpected error: %v", diags) + } + if gotUUID != newUUID { + t.Errorf("uuid inside create = %q, want %q (freshly resolved)", gotUUID, newUUID) + } + // ResourceData must hold the new UUID so late-init writes it to forProvider, + // preventing a UUID mismatch diff on the next reconciliation. + if got := d.Get("block_device.0.uuid"); got != newUUID { + t.Errorf("uuid in ResourceData after create = %v, want %q (for late-init)", got, newUUID) + } +} + +func serverState(attrs map[string]string) *terraform.InstanceState { + a := map[string]string{ + "id": "server-id", + "block_device.#": "1", + "block_device.0.source_type": "image", + "block_device.0.uuid": "1111", + } + for k, v := range attrs { + a[k] = v + } + + return &terraform.InstanceState{ID: "server-id", Attributes: a} +} + +// diffInputs builds the diff config and attaches its cty value to the state as +// RawConfig, the way upjet does, so diff suppress functions can read it. +func diffInputs(t *testing.T, res *schema.Resource, state *terraform.InstanceState, bd map[string]any) (*terraform.InstanceState, *terraform.ResourceConfig) { + t.Helper() + bd["source_type"] = "image" + raw := map[string]any{"block_device": []any{bd}} + v, err := schema.JSONMapToStateValue(raw, res.CoreConfigSchema()) + if err != nil { + t.Fatal(err) + } + if state != nil { + state.RawConfig = v + } + + return state, terraform.NewResourceConfigRaw(raw) +} + +func TestDiff(t *testing.T) { + res := testResource(nil, nil) + + cases := map[string]struct { + state *terraform.InstanceState + cfg map[string]any + wantDiff bool + wantNewKey bool + }{ + "NewResource": { + state: nil, + cfg: map[string]any{"image_name": "ubuntu"}, + wantDiff: true, + wantNewKey: true, + }, + "SameName": { + state: serverState(map[string]string{"block_device.0.image_name": "ubuntu"}), + cfg: map[string]any{"image_name": "ubuntu"}, + }, + "ChangedName": { + state: serverState(map[string]string{"block_device.0.image_name": "ubuntu"}), + cfg: map[string]any{"image_name": "debian"}, + wantDiff: true, + wantNewKey: true, + }, + "NoObservedName": { + state: serverState(nil), + cfg: map[string]any{"image_name": "ubuntu"}, + }, + "UUIDOnlyConfigWithObservedName": { + state: serverState(map[string]string{"block_device.0.image_name": "ubuntu"}), + cfg: map[string]any{"uuid": "1111"}, + }, + // The image was replaced under the same name and the server recreated + // with the new ID, while late initialization left the old ID in spec. + "StaleUUIDWithImageName": { + state: serverState(map[string]string{ + "block_device.0.uuid": "2222", + "block_device.0.image_name": "ubuntu", + }), + cfg: map[string]any{"image_name": "ubuntu", "uuid": "1111"}, + }, + "ChangedNameWithStaleUUID": { + state: serverState(map[string]string{"block_device.0.image_name": "ubuntu"}), + cfg: map[string]any{"image_name": "debian", "uuid": "1111"}, + wantDiff: true, + wantNewKey: true, + }, + "ChangedUUID": { + state: serverState(map[string]string{"block_device.0.image_name": "ubuntu"}), + cfg: map[string]any{"uuid": "2222"}, + wantDiff: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + state, cfg := diffInputs(t, res, tc.state, tc.cfg) + diff, err := res.Diff(context.Background(), state, cfg, nil) + if err != nil { + t.Fatal(err) + } + gotDiff := diff != nil && !diff.Empty() + if gotDiff != tc.wantDiff { + t.Fatalf("diff = %s, want diff: %v", diff.GoString(), tc.wantDiff) + } + if tc.wantNewKey && !diff.RequiresNew() { + t.Errorf("expected diff to require a new resource, got %v", diff) + } + }) + } +} + +func TestReadPopulatesImageName(t *testing.T) { + read := func(_ context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { + // Emulates the upstream read which rebuilds block_device without image_name. + _ = d.Set("block_device", []any{ + map[string]any{"source_type": "image", "uuid": "1111"}, + map[string]any{"source_type": "volume", "uuid": "vol"}, + }) + + return nil + } + lookups := 0 + namer := func(_ context.Context, _ *schema.ResourceData, _ any, id string) (string, error) { + lookups++ + if id != "1111" { + return "", errors.New("unexpected id " + id) + } + + return "ubuntu", nil + } + res := testResourceWithRead(nil, read, nil, namer) + + d := res.Data(serverState(nil)) + if diags := res.ReadContext(context.Background(), d, nil); diags.HasError() { + t.Fatalf("unexpected error: %v", diags) + } + if got := d.Get("block_device.0.image_name"); got != "ubuntu" { + t.Errorf("image_name = %v, want ubuntu", got) + } + if got := d.Get("block_device.1.image_name"); got != "" { + t.Errorf("volume block device must not get image_name, got %v", got) + } + if lookups != 1 { + t.Errorf("lookups = %d, want 1", lookups) + } +} + +func TestReadKeepsStateOnLookupError(t *testing.T) { + read := func(context.Context, *schema.ResourceData, any) diag.Diagnostics { return nil } + namer := func(context.Context, *schema.ResourceData, any, string) (string, error) { + return "", errors.New("not found") + } + res := testResourceWithRead(nil, read, nil, namer) + + d := res.Data(serverState(map[string]string{"block_device.0.image_name": "ubuntu"})) + if diags := res.ReadContext(context.Background(), d, nil); diags.HasError() { + t.Fatalf("lookup failure must not fail read: %v", diags) + } + if got := d.Get("block_device.0.image_name"); got != "ubuntu" { + t.Errorf("image_name = %v, want previous value ubuntu", got) + } +} diff --git a/config/namespaced/compute/config.go b/config/namespaced/compute/config.go index e430d8a..e0745af 100644 --- a/config/namespaced/compute/config.go +++ b/config/namespaced/compute/config.go @@ -1,6 +1,10 @@ package compute -import "github.com/crossplane/upjet/v2/pkg/config" +import ( + "github.com/crossplane/upjet/v2/pkg/config" + + "github.com/crossplane-contrib/provider-openstack/config/common/blockdevice" +) // Configure configures individual resources by adding custom ResourceConfigurators. func Configure(p *config.Provider) { @@ -23,6 +27,14 @@ func Configure(p *config.Provider) { r.References["security_groups"] = config.Reference{ TerraformName: "openstack_networking_secgroup_v2", } + // Allow block devices to use an image name instead of an image ID. + // The name is resolved to an ID by the provider at create time, so no + // ImageV2 object is required on the cluster (the reference is optional). + blockdevice.ConfigureImageName(r) + r.References["block_device.image_name"] = config.Reference{ + TerraformName: "openstack_images_image_v2", + Extractor: `github.com/crossplane/upjet/v2/pkg/resource.ExtractParamPath("name",false)`, + } }) p.AddResourceConfigurator("openstack_compute_quotaset_v2", func(r *config.Resource) { r.References["project_id"] = config.Reference{ diff --git a/go.mod b/go.mod index 7eaa4da..2dd1ec4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/crossplane/crossplane-tools v0.0.0-20251017183449-dd4517244339 github.com/crossplane/upjet/v2 v2.2.0 github.com/gophercloud/gophercloud/v2 v2.8.0 + github.com/gophercloud/utils/v2 v2.0.0-20250710092215-8f6f0255f600 github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-sdk/v2 v2.38.2 @@ -58,7 +59,6 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gophercloud/utils/v2 v2.0.0-20250710092215-8f6f0255f600 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect diff --git a/package/crds/compute.openstack.crossplane.io_instancev2s.yaml b/package/crds/compute.openstack.crossplane.io_instancev2s.yaml index 3bf9649..9a11f64 100644 --- a/package/crds/compute.openstack.crossplane.io_instancev2s.yaml +++ b/package/crds/compute.openstack.crossplane.io_instancev2s.yaml @@ -152,6 +152,88 @@ spec: must be less than or equal to the swap size of the flavor. Changing this creates a new server. type: string + imageName: + description: |- + The name of the + desired image for the server. Changing this rebuilds the existing server. + In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + type: string + imageNameRef: + description: Reference to a ImageV2 in images to populate + imageName. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + imageNameSelector: + description: Selector for a ImageV2 in images to populate + imageName. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching + labels is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object multiattach: description: |- Enable the attachment of multiattach-capable @@ -896,6 +978,88 @@ spec: must be less than or equal to the swap size of the flavor. Changing this creates a new server. type: string + imageName: + description: |- + The name of the + desired image for the server. Changing this rebuilds the existing server. + In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + type: string + imageNameRef: + description: Reference to a ImageV2 in images to populate + imageName. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + imageNameSelector: + description: Selector for a ImageV2 in images to populate + imageName. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching + labels is selected. + type: object + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object multiattach: description: |- Enable the attachment of multiattach-capable @@ -1723,6 +1887,12 @@ spec: must be less than or equal to the swap size of the flavor. Changing this creates a new server. type: string + imageName: + description: |- + The name of the + desired image for the server. Changing this rebuilds the existing server. + In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + type: string multiattach: description: |- Enable the attachment of multiattach-capable diff --git a/package/crds/compute.openstack.m.crossplane.io_instancev2s.yaml b/package/crds/compute.openstack.m.crossplane.io_instancev2s.yaml index 1b3e120..4d3ee59 100644 --- a/package/crds/compute.openstack.m.crossplane.io_instancev2s.yaml +++ b/package/crds/compute.openstack.m.crossplane.io_instancev2s.yaml @@ -133,6 +133,94 @@ spec: must be less than or equal to the swap size of the flavor. Changing this creates a new server. type: string + imageName: + description: |- + The name of the + desired image for the server. Changing this rebuilds the existing server. + In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + type: string + imageNameRef: + description: Reference to a ImageV2 in images to populate + imageName. + properties: + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + imageNameSelector: + description: Selector for a ImageV2 in images to populate + imageName. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching + labels is selected. + type: object + namespace: + description: Namespace for the selector + type: string + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object multiattach: description: |- Enable the attachment of multiattach-capable @@ -902,6 +990,94 @@ spec: must be less than or equal to the swap size of the flavor. Changing this creates a new server. type: string + imageName: + description: |- + The name of the + desired image for the server. Changing this rebuilds the existing server. + In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + type: string + imageNameRef: + description: Reference to a ImageV2 in images to populate + imageName. + properties: + name: + description: Name of the referenced object. + type: string + namespace: + description: Namespace of the referenced object + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + imageNameSelector: + description: Selector for a ImageV2 in images to populate + imageName. + properties: + matchControllerRef: + description: |- + MatchControllerRef ensures an object with the same controller reference + as the selecting object is selected. + type: boolean + matchLabels: + additionalProperties: + type: string + description: MatchLabels ensures an object with matching + labels is selected. + type: object + namespace: + description: Namespace for the selector + type: string + policy: + description: Policies for selection. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + type: object multiattach: description: |- Enable the attachment of multiattach-capable @@ -1731,6 +1907,12 @@ spec: must be less than or equal to the swap size of the flavor. Changing this creates a new server. type: string + imageName: + description: |- + The name of the + desired image for the server. Changing this rebuilds the existing server. + In a block device the name is resolved to the image ID before the server is created, so the image does not need to exist as a managed resource. Can only be used with source_type "image" and conflicts with uuid. Changing this creates a new server. + type: string multiattach: description: |- Enable the attachment of multiattach-capable