@@ -21,13 +21,20 @@ import (
2121 "fmt"
2222 "net/http"
2323 "os"
24+ "path/filepath"
2425 "sync"
2526 "time"
2627
2728 githubclient "github.com/SovereignCloudStack/cluster-stack-operator/pkg/github/client"
2829 "github.com/SovereignCloudStack/cluster-stack-operator/pkg/release"
2930 apiv1alpha1 "github.com/sovereignCloudStack/cluster-stack-provider-openstack/api/v1alpha1"
31+ "gopkg.in/yaml.v2"
32+ apierrors "k8s.io/apimachinery/pkg/api/errors"
33+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3034 "k8s.io/apimachinery/pkg/runtime"
35+ "k8s.io/apimachinery/pkg/runtime/schema"
36+ "k8s.io/apimachinery/pkg/types"
37+ "sigs.k8s.io/cluster-api/util/record"
3138 ctrl "sigs.k8s.io/controller-runtime"
3239 "sigs.k8s.io/controller-runtime/pkg/client"
3340 "sigs.k8s.io/controller-runtime/pkg/log"
@@ -42,6 +49,25 @@ type OpenStackClusterStackReleaseReconciler struct {
4249 openStackClusterStackRelDownloadDirectoryMutex sync.Mutex
4350}
4451
52+ // NodeImages is the list of OpenStack images for the given cluster stack release.
53+ type NodeImages struct {
54+ OpenStackImages []OpenStackImage `yaml:"openStackImages"`
55+ }
56+
57+ // OpenStackImage defines OpenStack image fields required for image upload.
58+ type OpenStackImage struct {
59+ Name string `yaml:"name"`
60+ URL string `yaml:"url"`
61+ DiskFormat string `yaml:"diskFormat"`
62+ ContainerFormat string `yaml:"containerFormat"`
63+ }
64+
65+ const (
66+ metadataFileName = "metadata.yaml"
67+ nodeImagesFileName = "node-images.yaml"
68+ maxNameLength = 63
69+ )
70+
4571//+kubebuilder:rbac:groups=infrastructure.clusterstack.x-k8s.io,resources=openstackclusterstackreleases,verbs=get;list;watch;create;update;patch;delete
4672//+kubebuilder:rbac:groups=infrastructure.clusterstack.x-k8s.io,resources=openstackclusterstackreleases/status,verbs=get;update;patch
4773//+kubebuilder:rbac:groups=infrastructure.clusterstack.x-k8s.io,resources=openstackclusterstackreleases/finalizers,verbs=update
@@ -92,17 +118,116 @@ func (r *OpenStackClusterStackReleaseReconciler) Reconcile(ctx context.Context,
92118 return ctrl.Result {Requeue : true }, nil
93119 }
94120
95- logger .Info ("OpenStackClusterStackRelease status" , "ready" , openstackclusterstackrelease .Status .Ready )
121+ nodeImages , err := getNodeImagesFromLocal (releaseAssets .LocalDownloadPath )
122+ if err != nil {
123+ return ctrl.Result {}, fmt .Errorf ("failed to get node images: %w" , err )
124+ }
125+ ownerRef := generateOwnerReference (openstackclusterstackrelease )
126+
127+ for _ , openStackImage := range nodeImages .OpenStackImages {
128+ osnirName := ensureMaxNameLength (fmt .Sprintf ("%s-%s" , openstackclusterstackrelease .Name , openStackImage .Name ))
129+ if err := r .getOrCreateOpenStackNodeImageRelease (ctx , openstackclusterstackrelease , osnirName , openStackImage , ownerRef ); err != nil {
130+ return ctrl.Result {}, fmt .Errorf ("failed to get or create OpenStackNodeImageRelease %s/%s: %w" , openstackclusterstackrelease .Namespace , osnirName , err )
131+ }
132+ }
133+
134+ ownedOpenStackNodeImageReleases , err := r .getOwnedOpenStackNodeImageReleases (ctx , openstackclusterstackrelease )
135+ if err != nil {
136+ return ctrl.Result {}, fmt .Errorf ("failed to get owned OpenStackNodeImageReleases: %w" , err )
137+ }
138+
139+ if len (ownedOpenStackNodeImageReleases ) == 0 {
140+ logger .Info ("OpenStackClusterStackRelease **not ready** yet, waiting for OpenStackNodeImageReleases to be created" )
141+ return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
142+ }
143+ for _ , openStackNodeImageRelease := range ownedOpenStackNodeImageReleases {
144+ if openStackNodeImageRelease .Status .Ready {
145+ continue
146+ }
147+ openstackclusterstackrelease .Status .Ready = false
148+ err = r .Status ().Update (ctx , openstackclusterstackrelease )
149+ if err != nil {
150+ return ctrl.Result {}, fmt .Errorf ("failed to update OpenStackClusterStackRelease status: %w" , err )
151+ }
152+
153+ logger .Info ("OpenStackClusterStackRelease **not ready** yet, waiting for OpenStackNodeImageRelease to be ready" , "name:" , openStackNodeImageRelease .ObjectMeta .Name , "ready:" , openStackNodeImageRelease .Status .Ready )
154+ return ctrl.Result {RequeueAfter : 30 * time .Second }, nil
155+ }
156+
96157 openstackclusterstackrelease .Status .Ready = true
97158 err = r .Status ().Update (ctx , openstackclusterstackrelease )
98159 if err != nil {
99- return ctrl.Result {}, fmt .Errorf ("failed to update OpenStackClusterStackRelease status" )
160+ return ctrl.Result {}, fmt .Errorf ("failed to update OpenStackClusterStackRelease status: %w" , err )
100161 }
101162 logger .Info ("OpenStackClusterStackRelease ready" )
102163
103164 return ctrl.Result {}, nil
104165}
105166
167+ func (r * OpenStackClusterStackReleaseReconciler ) getOrCreateOpenStackNodeImageRelease (ctx context.Context , openstackclusterstackrelease * apiv1alpha1.OpenStackClusterStackRelease , osnirName string , openStackImage OpenStackImage , ownerRef * metav1.OwnerReference ) error {
168+ openStackNodeImageRelease := & apiv1alpha1.OpenStackNodeImageRelease {}
169+
170+ err := r .Get (ctx , types.NamespacedName {Name : osnirName , Namespace : openstackclusterstackrelease .Namespace }, openStackNodeImageRelease )
171+
172+ // Nothing to do if the object exists
173+ if err == nil {
174+ return nil
175+ }
176+
177+ // Unexpected error
178+ if err != nil && ! apierrors .IsNotFound (err ) {
179+ return fmt .Errorf ("failed to get OpenStackNodeImageRelease: %w" , err )
180+ }
181+
182+ // Object not found - create it
183+ openStackNodeImageRelease .Name = osnirName
184+ openStackNodeImageRelease .Namespace = openstackclusterstackrelease .Namespace
185+ openStackNodeImageRelease .TypeMeta = metav1.TypeMeta {
186+ Kind : "OpenStackNodeImageRelease" ,
187+ APIVersion : "infrastructure.clusterstack.x-k8s.io/v1alpha1" ,
188+ }
189+ openStackNodeImageRelease .SetOwnerReferences ([]metav1.OwnerReference {* ownerRef })
190+ openStackNodeImageRelease .Spec .Name = openStackImage .Name
191+ openStackNodeImageRelease .Spec .URL = openStackImage .URL
192+ openStackNodeImageRelease .Spec .DiskFormat = openStackImage .DiskFormat
193+ openStackNodeImageRelease .Spec .ContainerFormat = openStackImage .ContainerFormat
194+ openStackNodeImageRelease .Spec .CloudName = openstackclusterstackrelease .Spec .CloudName
195+ openStackNodeImageRelease .Spec .IdentityRef = openstackclusterstackrelease .Spec .IdentityRef
196+
197+ if err := r .Create (ctx , openStackNodeImageRelease ); err != nil {
198+ record .Eventf (openStackNodeImageRelease ,
199+ "ErrorOpenStackNodeImageRelease" ,
200+ "failed to create %s OpenStackNodeImageRelease: %s" , osnirName , err .Error (),
201+ )
202+ return fmt .Errorf ("failed to create OpenStackNodeImageRelease: %w" , err )
203+ }
204+
205+ record .Eventf (openStackNodeImageRelease , "OpenStackNodeImageReleaseCreated" , "successfully created OpenStackNodeImageRelease object %q" , osnirName )
206+ return nil
207+ }
208+
209+ func (r * OpenStackClusterStackReleaseReconciler ) getOwnedOpenStackNodeImageReleases (ctx context.Context , openstackclusterstackrelease * apiv1alpha1.OpenStackClusterStackRelease ) ([]* apiv1alpha1.OpenStackNodeImageRelease , error ) {
210+ osnirList := & apiv1alpha1.OpenStackNodeImageReleaseList {}
211+
212+ if err := r .List (ctx , osnirList , client .InNamespace (openstackclusterstackrelease .Namespace )); err != nil {
213+ return nil , fmt .Errorf ("failed to list OpenStackNodeImageReleases: %w" , err )
214+ }
215+
216+ ownedOpenStackNodeImageReleases := make ([]* apiv1alpha1.OpenStackNodeImageRelease , 0 , len (osnirList .Items ))
217+
218+ for i := range osnirList .Items {
219+ osnir := osnirList .Items [i ]
220+ for i := range osnir .GetOwnerReferences () {
221+ ownerRef := osnir .GetOwnerReferences ()[i ]
222+ if matchOwnerReference (& ownerRef , openstackclusterstackrelease ) {
223+ ownedOpenStackNodeImageReleases = append (ownedOpenStackNodeImageReleases , & osnirList .Items [i ])
224+ break
225+ }
226+ }
227+ }
228+ return ownedOpenStackNodeImageReleases , nil
229+ }
230+
106231func downloadReleaseAssets (ctx context.Context , releaseTag , downloadPath string , gc githubclient.Client ) error {
107232 repoRelease , resp , err := gc .GetReleaseByTag (ctx , releaseTag )
108233 if err != nil {
@@ -112,7 +237,7 @@ func downloadReleaseAssets(ctx context.Context, releaseTag, downloadPath string,
112237 return fmt .Errorf ("failed to fetch release tag %s with status code %d" , releaseTag , resp .StatusCode )
113238 }
114239
115- assetlist := []string {"metadata.yaml" , "node-images.yaml" }
240+ assetlist := []string {metadataFileName , nodeImagesFileName }
116241
117242 if err := gc .DownloadReleaseAssets (ctx , repoRelease , downloadPath , assetlist ); err != nil {
118243 // if download failed for some reason, delete the release directory so that it can be retried in the next reconciliation
@@ -125,6 +250,48 @@ func downloadReleaseAssets(ctx context.Context, releaseTag, downloadPath string,
125250 return nil
126251}
127252
253+ func generateOwnerReference (openstackClusterStackRelease * apiv1alpha1.OpenStackClusterStackRelease ) * metav1.OwnerReference {
254+ return & metav1.OwnerReference {
255+ APIVersion : openstackClusterStackRelease .APIVersion ,
256+ Kind : openstackClusterStackRelease .Kind ,
257+ Name : openstackClusterStackRelease .Name ,
258+ UID : openstackClusterStackRelease .UID ,
259+ }
260+ }
261+
262+ func matchOwnerReference (a * metav1.OwnerReference , openstackclusterstackrelease * apiv1alpha1.OpenStackClusterStackRelease ) bool {
263+ aGV , err := schema .ParseGroupVersion (a .APIVersion )
264+ if err != nil {
265+ return false
266+ }
267+
268+ return aGV .Group == openstackclusterstackrelease .GroupVersionKind ().Group && a .Kind == openstackclusterstackrelease .Kind && a .Name == openstackclusterstackrelease .Name
269+ }
270+
271+ func getNodeImagesFromLocal (localDownloadPath string ) (* NodeImages , error ) {
272+ // Read the node-images.yaml file from the release
273+ nodeImagePath := filepath .Join (localDownloadPath , nodeImagesFileName )
274+ f , err := os .ReadFile (filepath .Clean (nodeImagePath ))
275+ if err != nil {
276+ return nil , fmt .Errorf ("failed to read node-images file %s: %w" , nodeImagePath , err )
277+ }
278+ nodeImages := NodeImages {}
279+ // if unmarshal fails, it indicates incomplete node-images file.
280+ // But we don't want to enforce download again.
281+ if err = yaml .Unmarshal (f , & nodeImages ); err != nil {
282+ return nil , fmt .Errorf ("failed to unmarshal node-images: %w" , err )
283+ }
284+ return & nodeImages , nil
285+ }
286+
287+ // TODO: Ensure RFC 1123 compatibility.
288+ func ensureMaxNameLength (base string ) string {
289+ if len (base ) > maxNameLength {
290+ return base [:maxNameLength ]
291+ }
292+ return base
293+ }
294+
128295// SetupWithManager sets up the controller with the Manager.
129296func (r * OpenStackClusterStackReleaseReconciler ) SetupWithManager (mgr ctrl.Manager ) error {
130297 return ctrl .NewControllerManagedBy (mgr ).
0 commit comments