diff --git a/api/compute/v1alpha1/machine_types.go b/api/compute/v1alpha1/machine_types.go index 0a8b88c99..98e6b5049 100644 --- a/api/compute/v1alpha1/machine_types.go +++ b/api/compute/v1alpha1/machine_types.go @@ -160,6 +160,8 @@ type VolumeStatus struct { State VolumeState `json:"state,omitempty"` // LastStateTransitionTime is the last time the State transitioned. LastStateTransitionTime *metav1.Time `json:"lastStateTransitionTime,omitempty"` + // VolumeRef reference to the claimed Volume + VolumeRef corev1.LocalObjectReference `json:"volumeRef,omitempty"` } // VolumeState is the infrastructure attachment state a Volume can be in. diff --git a/api/compute/v1alpha1/zz_generated.deepcopy.go b/api/compute/v1alpha1/zz_generated.deepcopy.go index ec0e8e6a0..f10cdb620 100644 --- a/api/compute/v1alpha1/zz_generated.deepcopy.go +++ b/api/compute/v1alpha1/zz_generated.deepcopy.go @@ -672,6 +672,7 @@ func (in *VolumeStatus) DeepCopyInto(out *VolumeStatus) { in, out := &in.LastStateTransitionTime, &out.LastStateTransitionTime *out = (*in).DeepCopy() } + out.VolumeRef = in.VolumeRef return } diff --git a/client-go/applyconfigurations/compute/v1alpha1/volumestatus.go b/client-go/applyconfigurations/compute/v1alpha1/volumestatus.go index 4b1eab904..032b57d99 100644 --- a/client-go/applyconfigurations/compute/v1alpha1/volumestatus.go +++ b/client-go/applyconfigurations/compute/v1alpha1/volumestatus.go @@ -7,16 +7,18 @@ package v1alpha1 import ( v1alpha1 "github.com/ironcore-dev/ironcore/api/compute/v1alpha1" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // VolumeStatusApplyConfiguration represents an declarative configuration of the VolumeStatus type for use // with apply. type VolumeStatusApplyConfiguration struct { - Name *string `json:"name,omitempty"` - Handle *string `json:"handle,omitempty"` - State *v1alpha1.VolumeState `json:"state,omitempty"` - LastStateTransitionTime *v1.Time `json:"lastStateTransitionTime,omitempty"` + Name *string `json:"name,omitempty"` + Handle *string `json:"handle,omitempty"` + State *v1alpha1.VolumeState `json:"state,omitempty"` + LastStateTransitionTime *v1.Time `json:"lastStateTransitionTime,omitempty"` + VolumeRef *corev1.LocalObjectReference `json:"volumeRef,omitempty"` } // VolumeStatusApplyConfiguration constructs an declarative configuration of the VolumeStatus type for use with @@ -56,3 +58,11 @@ func (b *VolumeStatusApplyConfiguration) WithLastStateTransitionTime(value v1.Ti b.LastStateTransitionTime = &value return b } + +// WithVolumeRef sets the VolumeRef 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 VolumeRef field is set to the value of the last call. +func (b *VolumeStatusApplyConfiguration) WithVolumeRef(value corev1.LocalObjectReference) *VolumeStatusApplyConfiguration { + b.VolumeRef = &value + return b +} diff --git a/client-go/applyconfigurations/internal/internal.go b/client-go/applyconfigurations/internal/internal.go index a1cd532d3..39691282b 100644 --- a/client-go/applyconfigurations/internal/internal.go +++ b/client-go/applyconfigurations/internal/internal.go @@ -433,6 +433,10 @@ var schemaYAML = typed.YAMLObject(`types: - name: state type: scalar: string + - name: volumeRef + type: + namedType: io.k8s.api.core.v1.LocalObjectReference + default: {} - name: com.github.ironcore-dev.ironcore.api.core.v1alpha1.ObjectSelector map: fields: diff --git a/client-go/openapi/zz_generated.openapi.go b/client-go/openapi/zz_generated.openapi.go index cd93b1142..f2250b2ed 100644 --- a/client-go/openapi/zz_generated.openapi.go +++ b/client-go/openapi/zz_generated.openapi.go @@ -1840,12 +1840,19 @@ func schema_ironcore_api_compute_v1alpha1_VolumeStatus(ref common.ReferenceCallb Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), }, }, + "volumeRef": { + SchemaProps: spec.SchemaProps{ + Description: "VolumeRef reference to the claimed Volume", + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/core/v1.LocalObjectReference"), + }, + }, }, Required: []string{"name"}, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "k8s.io/api/core/v1.LocalObjectReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/gen/swagger.json b/gen/swagger.json index c2af796c1..7fba961c3 100644 --- a/gen/swagger.json +++ b/gen/swagger.json @@ -65371,6 +65371,10 @@ "state": { "description": "State represents the attachment state of a Volume.", "type": "string" + }, + "volumeRef": { + "description": "VolumeRef reference to the claimed Volume", + "$ref": "#/definitions/io.k8s.api.core.v1.LocalObjectReference" } } }, diff --git a/gen/v3/apis__compute.ironcore.dev__v1alpha1_openapi.json b/gen/v3/apis__compute.ironcore.dev__v1alpha1_openapi.json index 9e3d858cb..9a5c31b63 100644 --- a/gen/v3/apis__compute.ironcore.dev__v1alpha1_openapi.json +++ b/gen/v3/apis__compute.ironcore.dev__v1alpha1_openapi.json @@ -4883,6 +4883,15 @@ "state": { "description": "State represents the attachment state of a Volume.", "type": "string" + }, + "volumeRef": { + "description": "VolumeRef reference to the claimed Volume", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.api.core.v1.LocalObjectReference" + } + ] } } }, diff --git a/internal/apis/compute/machine_types.go b/internal/apis/compute/machine_types.go index ca9571ae3..7f9193ab3 100644 --- a/internal/apis/compute/machine_types.go +++ b/internal/apis/compute/machine_types.go @@ -150,6 +150,8 @@ type VolumeStatus struct { State VolumeState // LastStateTransitionTime is the last time the State transitioned. LastStateTransitionTime *metav1.Time + //VolumeRef reference to the claimed Volume + VolumeRef corev1.LocalObjectReference } // VolumeState is the infrastructure attachment state a Volume can be in. diff --git a/internal/apis/compute/v1alpha1/zz_generated.conversion.go b/internal/apis/compute/v1alpha1/zz_generated.conversion.go index 81d3f0fa4..fd4396e12 100644 --- a/internal/apis/compute/v1alpha1/zz_generated.conversion.go +++ b/internal/apis/compute/v1alpha1/zz_generated.conversion.go @@ -970,6 +970,7 @@ func autoConvert_v1alpha1_VolumeStatus_To_compute_VolumeStatus(in *v1alpha1.Volu out.Handle = in.Handle out.State = compute.VolumeState(in.State) out.LastStateTransitionTime = (*metav1.Time)(unsafe.Pointer(in.LastStateTransitionTime)) + out.VolumeRef = in.VolumeRef return nil } @@ -983,6 +984,7 @@ func autoConvert_compute_VolumeStatus_To_v1alpha1_VolumeStatus(in *compute.Volum out.Handle = in.Handle out.State = v1alpha1.VolumeState(in.State) out.LastStateTransitionTime = (*metav1.Time)(unsafe.Pointer(in.LastStateTransitionTime)) + out.VolumeRef = in.VolumeRef return nil } diff --git a/internal/apis/compute/zz_generated.deepcopy.go b/internal/apis/compute/zz_generated.deepcopy.go index 086786056..34d3e7bee 100644 --- a/internal/apis/compute/zz_generated.deepcopy.go +++ b/internal/apis/compute/zz_generated.deepcopy.go @@ -667,6 +667,7 @@ func (in *VolumeStatus) DeepCopyInto(out *VolumeStatus) { in, out := &in.LastStateTransitionTime, &out.LastStateTransitionTime *out = (*in).DeepCopy() } + out.VolumeRef = in.VolumeRef return } diff --git a/internal/app/app_suite_test.go b/internal/app/app_suite_test.go index 07349b0f7..d7735a287 100644 --- a/internal/app/app_suite_test.go +++ b/internal/app/app_suite_test.go @@ -32,7 +32,7 @@ import ( const ( pollingInterval = 50 * time.Millisecond - eventuallyTimeout = 3 * time.Second + eventuallyTimeout = 5 * time.Second consistentlyDuration = 1 * time.Second apiServiceTimeout = 5 * time.Minute ) diff --git a/internal/app/core_test.go b/internal/app/core_test.go index 95e9251d1..efb6b5e0b 100644 --- a/internal/app/core_test.go +++ b/internal/app/core_test.go @@ -72,14 +72,14 @@ var _ = Describe("Core", func() { } Expect(k8sClient.Create(ctx, machine)).To(Succeed()) - By("getting the resource quota") + By("waiting for the resource quota to be updated") resourceQuotaKey := client.ObjectKeyFromObject(resourceQuota) - Expect(k8sClient.Get(ctx, resourceQuotaKey, resourceQuota)).To(Succeed()) - - By("inspecting the resource quota") - Expect(resourceQuota.Status.Used).To(Equal(corev1alpha1.ResourceList{ - corev1alpha1.ResourceRequestsCPU: resource.MustParse("1"), - })) + Eventually(func(g Gomega) { + Expect(k8sClient.Get(ctx, resourceQuotaKey, resourceQuota)).To(Succeed()) + g.Expect(resourceQuota.Status.Used).To(Equal(corev1alpha1.ResourceList{ + corev1alpha1.ResourceRequestsCPU: resource.MustParse("1"), + })) + }).Should(Succeed()) }) }) }) diff --git a/poollet/machinepoollet/controllers/machine_controller_test.go b/poollet/machinepoollet/controllers/machine_controller_test.go index b1146a3f6..ce3439161 100644 --- a/poollet/machinepoollet/controllers/machine_controller_test.go +++ b/poollet/machinepoollet/controllers/machine_controller_test.go @@ -20,6 +20,7 @@ import ( . "github.com/onsi/gomega/gstruct" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" . "sigs.k8s.io/controller-runtime/pkg/envtest/komega" ) @@ -325,6 +326,13 @@ var _ = Describe("MachineController", func() { Eventually(srv.Machines[iriMachine.Metadata.Id]).Should(SatisfyAll( HaveField("Spec.NetworkInterfaces", BeEmpty()), )) + + By("Verifying ironcore machine volume status with correct volume reference") + Eventually(Object(machine)).Should(HaveField("Status.Volumes", ConsistOf(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("primary"), + "State": Equal(computev1alpha1.VolumeStatePending), + "VolumeRef": Equal(corev1.LocalObjectReference{Name: volume.Name}), + })))) }) It("should correctly manage the power state of a machine", func(ctx SpecContext) { @@ -392,6 +400,98 @@ var _ = Describe("MachineController", func() { srv.SetMachines([]*testingmachine.FakeMachine{iriMachine}) Eventually(Object(machine)).Should(HaveField("Status.State", Equal(computev1alpha1.MachineStateTerminating))) }) + + It("should create a machine and verify claimed volume reference with ephemeral volume", func(ctx SpecContext) { + By("creating a machine") + const fooAnnotationValue = "bar" + machine := &computev1alpha1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + GenerateName: "machine-", + Annotations: map[string]string{ + fooAnnotation: fooAnnotationValue, + }, + }, + Spec: computev1alpha1.MachineSpec{ + MachineClassRef: corev1.LocalObjectReference{Name: mc.Name}, + MachinePoolRef: &corev1.LocalObjectReference{Name: mp.Name}, + Volumes: []computev1alpha1.Volume{ + { + Name: "primary", + VolumeSource: computev1alpha1.VolumeSource{ + Ephemeral: &computev1alpha1.EphemeralVolumeSource{ + VolumeTemplate: &storagev1alpha1.VolumeTemplateSpec{ + Spec: storagev1alpha1.VolumeSpec{}, + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, machine)).To(Succeed()) + By("By getting ephimeral volume") + volumeKey := types.NamespacedName{ + Namespace: ns.Name, + Name: computev1alpha1.MachineEphemeralVolumeName(machine.Name, "primary"), + } + ephimeralVolume := &storagev1alpha1.Volume{} + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, volumeKey, ephimeralVolume) + Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred()) + g.Expect(err).NotTo(HaveOccurred()) + }).Should(Succeed()) + + By("patching the volume to be available") + Eventually(UpdateStatus(ephimeralVolume, func() { + ephimeralVolume.Status.State = storagev1alpha1.VolumeStateAvailable + ephimeralVolume.Status.Access = &storagev1alpha1.VolumeAccess{ + Driver: "test", + Handle: "testhandle", + } + })).Should(Succeed()) + + By("waiting for the runtime to report the machine and volume") + Eventually(srv).Should(SatisfyAll( + HaveField("Machines", HaveLen(1)), + )) + _, iriMachine := GetSingleMapEntry(srv.Machines) + + By("inspecting the iri machine") + Expect(iriMachine.Metadata.Labels).To(HaveKeyWithValue(machinepoolletv1alpha1.DownwardAPILabel(fooDownwardAPILabel), fooAnnotationValue)) + Expect(iriMachine.Spec.Class).To(Equal(mc.Name)) + Expect(iriMachine.Spec.Power).To(Equal(iri.Power_POWER_ON)) + Expect(iriMachine.Spec.Volumes).To(ConsistOf(&iri.Volume{ + Name: "primary", + Device: "oda", + Connection: &iri.VolumeConnection{ + Driver: "test", + Handle: "testhandle", + }, + })) + + By("waiting for the ironcore machine status to be up-to-date") + expectedMachineID := machinepoolletmachine.MakeID(testingmachine.FakeRuntimeName, iriMachine.Metadata.Id) + Eventually(Object(machine)).Should(SatisfyAll( + HaveField("Status.MachineID", expectedMachineID.String()), + HaveField("Status.ObservedGeneration", machine.Generation), + )) + + By("setting the network interface id in the machine status") + iriMachine = &testingmachine.FakeMachine{Machine: *proto.Clone(&iriMachine.Machine).(*iri.Machine)} + iriMachine.Metadata.Generation = 1 + iriMachine.Status.ObservedGeneration = 1 + + srv.SetMachines([]*testingmachine.FakeMachine{iriMachine}) + + By("Verifying ironcore machine volume status with correct volume reference") + Eventually(Object(machine)).Should(HaveField("Status.Volumes", ConsistOf(MatchFields(IgnoreExtras, Fields{ + "Name": Equal("primary"), + "State": Equal(computev1alpha1.VolumeStatePending), + "VolumeRef": Equal(corev1.LocalObjectReference{Name: ephimeralVolume.Name}), + })))) + }) + }) func GetSingleMapEntry[K comparable, V any](m map[K]V) (K, V) { diff --git a/poollet/machinepoollet/controllers/machine_controller_volume.go b/poollet/machinepoollet/controllers/machine_controller_volume.go index b36ccd08e..42b559bc2 100644 --- a/poollet/machinepoollet/controllers/machine_controller_volume.go +++ b/poollet/machinepoollet/controllers/machine_controller_volume.go @@ -347,16 +347,18 @@ func (r *MachineReconciler) getVolumeStatusesForMachine( iriVolumeStatus, ok = iriVolumeStatusByName[machineVolume.Name] volumeStatusValues computev1alpha1.VolumeStatus ) + volumeName := computev1alpha1.MachineVolumeName(machine.Name, machineVolume) if ok { var err error - volumeStatusValues, err = r.convertIRIVolumeStatus(iriVolumeStatus) + volumeStatusValues, err = r.convertIRIVolumeStatus(iriVolumeStatus, volumeName) if err != nil { return nil, fmt.Errorf("[volume %s] %w", machineVolume.Name, err) } } else { volumeStatusValues = computev1alpha1.VolumeStatus{ - Name: machineVolume.Name, - State: computev1alpha1.VolumeStatePending, + Name: machineVolume.Name, + State: computev1alpha1.VolumeStatePending, + VolumeRef: corev1.LocalObjectReference{Name: volumeName}, } } @@ -382,16 +384,17 @@ func (r *MachineReconciler) convertIRIVolumeState(iriState iri.VolumeState) (com return "", fmt.Errorf("unknown iri volume state %v", iriState) } -func (r *MachineReconciler) convertIRIVolumeStatus(iriVolumeStatus *iri.VolumeStatus) (computev1alpha1.VolumeStatus, error) { +func (r *MachineReconciler) convertIRIVolumeStatus(iriVolumeStatus *iri.VolumeStatus, volumeName string) (computev1alpha1.VolumeStatus, error) { state, err := r.convertIRIVolumeState(iriVolumeStatus.State) if err != nil { return computev1alpha1.VolumeStatus{}, err } return computev1alpha1.VolumeStatus{ - Name: iriVolumeStatus.Name, - Handle: iriVolumeStatus.Handle, - State: state, + Name: iriVolumeStatus.Name, + Handle: iriVolumeStatus.Handle, + State: state, + VolumeRef: corev1.LocalObjectReference{Name: volumeName}, }, nil } @@ -402,4 +405,5 @@ func (r *MachineReconciler) addVolumeStatusValues(now metav1.Time, existing, new existing.Name = newValues.Name existing.State = newValues.State existing.Handle = newValues.Handle + existing.VolumeRef = newValues.VolumeRef }