Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.

Commit 668228d

Browse files
authored
Add OpenStackNodeImageRelease creation logic in OpenStackClusterStackRelease controller (#25)
Signed-off-by: Matej Feder <[email protected]>
1 parent 87c2426 commit 668228d

File tree

22 files changed

+16200
-7
lines changed

22 files changed

+16200
-7
lines changed

Tiltfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ def deploy_capo():
7575

7676
def prepare_environment():
7777
local("kubectl create namespace cluster --dry-run=client -o yaml | kubectl apply -f -")
78-
78+
# Delete CSO validating webhook
79+
local("kubectl delete validatingwebhookconfiguration cso-validating-webhook-configuration")
7980
# if it's already present then don't copy
8081
# if not os.path.exists('.clusterstack.yaml'):
8182
# local("cp config/cspo/clusterstack.yaml .clusterstack.yaml")

api/v1alpha1/openstacknodeimagerelease_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ type OpenStackNodeImageReleaseSpec struct {
3030
Name string `json:"name"`
3131
// The URL of the node image
3232
URL string `json:"url"`
33+
// The DiskFormat of the node image
34+
DiskFormat string `json:"diskFormat"`
35+
// The ContainerFormat of the node image
36+
ContainerFormat string `json:"containerFormat"`
3337
// The name of the cloud to use from the clouds secret
3438
CloudName string `json:"cloudName"`
3539
// IdentityRef is a reference to a identity to be used when reconciling this cluster
@@ -45,6 +49,8 @@ type OpenStackNodeImageReleaseStatus struct {
4549

4650
//+kubebuilder:object:root=true
4751
//+kubebuilder:subresource:status
52+
//+kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready"
53+
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of OpenStackNodeImageRelease"
4854

4955
// OpenStackNodeImageRelease is the Schema for the openstacknodeimagereleases API.
5056
type OpenStackNodeImageRelease struct {

config/crd/bases/infrastructure.clusterstack.x-k8s.io_openstacknodeimagereleases.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ spec:
1414
singular: openstacknodeimagerelease
1515
scope: Namespaced
1616
versions:
17-
- name: v1alpha1
17+
- additionalPrinterColumns:
18+
- jsonPath: .status.ready
19+
name: Ready
20+
type: boolean
21+
- description: Time duration since creation of OpenStackNodeImageRelease
22+
jsonPath: .metadata.creationTimestamp
23+
name: Age
24+
type: date
25+
name: v1alpha1
1826
schema:
1927
openAPIV3Schema:
2028
description: OpenStackNodeImageRelease is the Schema for the openstacknodeimagereleases
@@ -39,6 +47,12 @@ spec:
3947
cloudName:
4048
description: The name of the cloud to use from the clouds secret
4149
type: string
50+
containerFormat:
51+
description: The ContainerFormat of the node image
52+
type: string
53+
diskFormat:
54+
description: The DiskFormat of the node image
55+
type: string
4256
identityRef:
4357
description: IdentityRef is a reference to a identity to be used when
4458
reconciling this cluster
@@ -65,6 +79,8 @@ spec:
6579
type: string
6680
required:
6781
- cloudName
82+
- containerFormat
83+
- diskFormat
6884
- name
6985
- url
7086
type: object

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ require (
66
github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.1
77
github.com/onsi/ginkgo/v2 v2.13.2
88
github.com/onsi/gomega v1.30.0
9+
gopkg.in/yaml.v2 v2.4.0
910
k8s.io/apimachinery v0.28.4
1011
k8s.io/client-go v0.28.4
12+
sigs.k8s.io/cluster-api v1.6.0
1113
sigs.k8s.io/cluster-api-provider-openstack v0.9.0
1214
sigs.k8s.io/controller-runtime v0.16.3
1315
)
@@ -68,15 +70,13 @@ require (
6870
google.golang.org/appengine v1.6.7 // indirect
6971
google.golang.org/protobuf v1.31.0 // indirect
7072
gopkg.in/inf.v0 v0.9.1 // indirect
71-
gopkg.in/yaml.v2 v2.4.0 // indirect
7273
gopkg.in/yaml.v3 v3.0.1 // indirect
7374
k8s.io/api v0.28.4 // indirect
7475
k8s.io/apiextensions-apiserver v0.28.4 // indirect
7576
k8s.io/component-base v0.28.4 // indirect
7677
k8s.io/klog/v2 v2.100.1 // indirect
7778
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
7879
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
79-
sigs.k8s.io/cluster-api v1.6.0 // indirect
8080
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
8181
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
8282
sigs.k8s.io/yaml v1.4.0 // indirect

internal/controller/openstackclusterstackrelease_controller.go

Lines changed: 170 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
106231
func 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.
129296
func (r *OpenStackClusterStackReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
130297
return ctrl.NewControllerManagedBy(mgr).

0 commit comments

Comments
 (0)