Skip to content

Commit

Permalink
Add OpenStackNodeImageRelease creation logic in OpenStackClusterStack…
Browse files Browse the repository at this point in the history
…Release controller

Signed-off-by: Matej Feder <matej.feder@dnation.cloud>
  • Loading branch information
matofeder committed Dec 27, 2023
1 parent 87c2426 commit 7571463
Show file tree
Hide file tree
Showing 22 changed files with 16,200 additions and 7 deletions.
3 changes: 2 additions & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def deploy_capo():

def prepare_environment():
local("kubectl create namespace cluster --dry-run=client -o yaml | kubectl apply -f -")

# Delete CSO validating webhook
local("kubectl delete validatingwebhookconfiguration cso-validating-webhook-configuration")
# if it's already present then don't copy
# if not os.path.exists('.clusterstack.yaml'):
# local("cp config/cspo/clusterstack.yaml .clusterstack.yaml")
Expand Down
6 changes: 6 additions & 0 deletions api/v1alpha1/openstacknodeimagerelease_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type OpenStackNodeImageReleaseSpec struct {
Name string `json:"name"`
// The URL of the node image
URL string `json:"url"`
// The DiskFormat of the node image
DiskFormat string `json:"diskFormat"`
// The ContainerFormat of the node image
ContainerFormat string `json:"containerFormat"`
// The name of the cloud to use from the clouds secret
CloudName string `json:"cloudName"`
// IdentityRef is a reference to a identity to be used when reconciling this cluster
Expand All @@ -45,6 +49,8 @@ type OpenStackNodeImageReleaseStatus struct {

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

// OpenStackNodeImageRelease is the Schema for the openstacknodeimagereleases API.
type OpenStackNodeImageRelease struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ spec:
singular: openstacknodeimagerelease
scope: Namespaced
versions:
- name: v1alpha1
- additionalPrinterColumns:
- jsonPath: .status.ready
name: Ready
type: boolean
- description: Time duration since creation of OpenStackNodeImageRelease
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: OpenStackNodeImageRelease is the Schema for the openstacknodeimagereleases
Expand All @@ -39,6 +47,12 @@ spec:
cloudName:
description: The name of the cloud to use from the clouds secret
type: string
containerFormat:
description: The ContainerFormat of the node image
type: string
diskFormat:
description: The DiskFormat of the node image
type: string
identityRef:
description: IdentityRef is a reference to a identity to be used when
reconciling this cluster
Expand All @@ -65,6 +79,8 @@ spec:
type: string
required:
- cloudName
- containerFormat
- diskFormat
- name
- url
type: object
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ require (
github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.1
github.com/onsi/ginkgo/v2 v2.13.2
github.com/onsi/gomega v1.30.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/apimachinery v0.28.4
k8s.io/client-go v0.28.4
sigs.k8s.io/cluster-api v1.6.0
sigs.k8s.io/cluster-api-provider-openstack v0.9.0
sigs.k8s.io/controller-runtime v0.16.3
)
Expand Down Expand Up @@ -68,15 +70,13 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.28.4 // indirect
k8s.io/apiextensions-apiserver v0.28.4 // indirect
k8s.io/component-base v0.28.4 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
sigs.k8s.io/cluster-api v1.6.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
Expand Down
173 changes: 170 additions & 3 deletions internal/controller/openstackclusterstackrelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,20 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"sync"
"time"

githubclient "github.com/SovereignCloudStack/cluster-stack-operator/pkg/github/client"
"github.com/SovereignCloudStack/cluster-stack-operator/pkg/release"
apiv1alpha1 "github.com/sovereignCloudStack/cluster-stack-provider-openstack/api/v1alpha1"
"gopkg.in/yaml.v2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/cluster-api/util/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
Expand All @@ -42,6 +49,25 @@ type OpenStackClusterStackReleaseReconciler struct {
openStackClusterStackRelDownloadDirectoryMutex sync.Mutex
}

// NodeImages is the list of OpenStack images for the given cluster stack release.
type NodeImages struct {
OpenStackImages []OpenStackImage `yaml:"openStackImages"`
}

// OpenStackImage defines OpenStack image fields required for image upload.
type OpenStackImage struct {
Name string `yaml:"name"`
URL string `yaml:"url"`
DiskFormat string `yaml:"diskFormat"`
ContainerFormat string `yaml:"containerFormat"`
}

const (
metadataFileName = "metadata.yaml"
nodeImagesFileName = "node-images.yaml"
maxNameLength = 63
)

//+kubebuilder:rbac:groups=infrastructure.clusterstack.x-k8s.io,resources=openstackclusterstackreleases,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=infrastructure.clusterstack.x-k8s.io,resources=openstackclusterstackreleases/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=infrastructure.clusterstack.x-k8s.io,resources=openstackclusterstackreleases/finalizers,verbs=update
Expand Down Expand Up @@ -92,17 +118,116 @@ func (r *OpenStackClusterStackReleaseReconciler) Reconcile(ctx context.Context,
return ctrl.Result{Requeue: true}, nil
}

logger.Info("OpenStackClusterStackRelease status", "ready", openstackclusterstackrelease.Status.Ready)
nodeImages, err := getNodeImagesFromLocal(releaseAssets.LocalDownloadPath)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to get node images: %w", err)
}
ownerRef := generateOwnerReference(openstackclusterstackrelease)

for _, openStackImage := range nodeImages.OpenStackImages {
osnirName := ensureMaxNameLength(fmt.Sprintf("%s-%s", openstackclusterstackrelease.Name, openStackImage.Name))
if err := r.getOrCreateOpenStackNodeImageRelease(ctx, openstackclusterstackrelease, osnirName, openStackImage, ownerRef); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to get or create OpenStackNodeImageRelease %s/%s: %w", openstackclusterstackrelease.Namespace, osnirName, err)
}
}

ownedOpenStackNodeImageReleases, err := r.getOwnedOpenStackNodeImageReleases(ctx, openstackclusterstackrelease)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to get owned OpenStackNodeImageReleases: %w", err)
}

if len(ownedOpenStackNodeImageReleases) == 0 {
logger.Info("OpenStackClusterStackRelease **not ready** yet, waiting for OpenStackNodeImageReleases to be created")
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
for _, openStackNodeImageRelease := range ownedOpenStackNodeImageReleases {
if openStackNodeImageRelease.Status.Ready {
continue
}
openstackclusterstackrelease.Status.Ready = false
err = r.Status().Update(ctx, openstackclusterstackrelease)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to update OpenStackClusterStackRelease status: %w", err)
}

logger.Info("OpenStackClusterStackRelease **not ready** yet, waiting for OpenStackNodeImageRelease to be ready", "name:", openStackNodeImageRelease.ObjectMeta.Name, "ready:", openStackNodeImageRelease.Status.Ready)
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}

openstackclusterstackrelease.Status.Ready = true
err = r.Status().Update(ctx, openstackclusterstackrelease)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to update OpenStackClusterStackRelease status")
return ctrl.Result{}, fmt.Errorf("failed to update OpenStackClusterStackRelease status: %w", err)
}
logger.Info("OpenStackClusterStackRelease ready")

return ctrl.Result{}, nil
}

func (r *OpenStackClusterStackReleaseReconciler) getOrCreateOpenStackNodeImageRelease(ctx context.Context, openstackclusterstackrelease *apiv1alpha1.OpenStackClusterStackRelease, osnirName string, openStackImage OpenStackImage, ownerRef *metav1.OwnerReference) error {
openStackNodeImageRelease := &apiv1alpha1.OpenStackNodeImageRelease{}

err := r.Get(ctx, types.NamespacedName{Name: osnirName, Namespace: openstackclusterstackrelease.Namespace}, openStackNodeImageRelease)

// Nothing to do if the object exists
if err == nil {
return nil
}

// Unexpected error
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to get OpenStackNodeImageRelease: %w", err)
}

// Object not found - create it
openStackNodeImageRelease.Name = osnirName
openStackNodeImageRelease.Namespace = openstackclusterstackrelease.Namespace
openStackNodeImageRelease.TypeMeta = metav1.TypeMeta{
Kind: "OpenStackNodeImageRelease",
APIVersion: "infrastructure.clusterstack.x-k8s.io/v1alpha1",
}
openStackNodeImageRelease.SetOwnerReferences([]metav1.OwnerReference{*ownerRef})
openStackNodeImageRelease.Spec.Name = openStackImage.Name
openStackNodeImageRelease.Spec.URL = openStackImage.URL
openStackNodeImageRelease.Spec.DiskFormat = openStackImage.DiskFormat
openStackNodeImageRelease.Spec.ContainerFormat = openStackImage.ContainerFormat
openStackNodeImageRelease.Spec.CloudName = openstackclusterstackrelease.Spec.CloudName
openStackNodeImageRelease.Spec.IdentityRef = openstackclusterstackrelease.Spec.IdentityRef

if err := r.Create(ctx, openStackNodeImageRelease); err != nil {
record.Eventf(openStackNodeImageRelease,
"ErrorOpenStackNodeImageRelease",
"failed to create %s OpenStackNodeImageRelease: %s", osnirName, err.Error(),
)
return fmt.Errorf("failed to create OpenStackNodeImageRelease: %w", err)
}

record.Eventf(openStackNodeImageRelease, "OpenStackNodeImageReleaseCreated", "successfully created OpenStackNodeImageRelease object %q", osnirName)
return nil
}

func (r *OpenStackClusterStackReleaseReconciler) getOwnedOpenStackNodeImageReleases(ctx context.Context, openstackclusterstackrelease *apiv1alpha1.OpenStackClusterStackRelease) ([]*apiv1alpha1.OpenStackNodeImageRelease, error) {
osnirList := &apiv1alpha1.OpenStackNodeImageReleaseList{}

if err := r.List(ctx, osnirList, client.InNamespace(openstackclusterstackrelease.Namespace)); err != nil {
return nil, fmt.Errorf("failed to list OpenStackNodeImageReleases: %w", err)
}

ownedOpenStackNodeImageReleases := make([]*apiv1alpha1.OpenStackNodeImageRelease, 0, len(osnirList.Items))

for i := range osnirList.Items {
osnir := osnirList.Items[i]
for i := range osnir.GetOwnerReferences() {
ownerRef := osnir.GetOwnerReferences()[i]
if matchOwnerReference(&ownerRef, openstackclusterstackrelease) {
ownedOpenStackNodeImageReleases = append(ownedOpenStackNodeImageReleases, &osnirList.Items[i])
break
}
}
}
return ownedOpenStackNodeImageReleases, nil
}

func downloadReleaseAssets(ctx context.Context, releaseTag, downloadPath string, gc githubclient.Client) error {
repoRelease, resp, err := gc.GetReleaseByTag(ctx, releaseTag)
if err != nil {
Expand All @@ -112,7 +237,7 @@ func downloadReleaseAssets(ctx context.Context, releaseTag, downloadPath string,
return fmt.Errorf("failed to fetch release tag %s with status code %d", releaseTag, resp.StatusCode)
}

assetlist := []string{"metadata.yaml", "node-images.yaml"}
assetlist := []string{metadataFileName, nodeImagesFileName}

if err := gc.DownloadReleaseAssets(ctx, repoRelease, downloadPath, assetlist); err != nil {
// if download failed for some reason, delete the release directory so that it can be retried in the next reconciliation
Expand All @@ -125,6 +250,48 @@ func downloadReleaseAssets(ctx context.Context, releaseTag, downloadPath string,
return nil
}

func generateOwnerReference(openstackClusterStackRelease *apiv1alpha1.OpenStackClusterStackRelease) *metav1.OwnerReference {
return &metav1.OwnerReference{
APIVersion: openstackClusterStackRelease.APIVersion,
Kind: openstackClusterStackRelease.Kind,
Name: openstackClusterStackRelease.Name,
UID: openstackClusterStackRelease.UID,
}
}

func matchOwnerReference(a *metav1.OwnerReference, openstackclusterstackrelease *apiv1alpha1.OpenStackClusterStackRelease) bool {
aGV, err := schema.ParseGroupVersion(a.APIVersion)
if err != nil {
return false
}

return aGV.Group == openstackclusterstackrelease.GroupVersionKind().Group && a.Kind == openstackclusterstackrelease.Kind && a.Name == openstackclusterstackrelease.Name
}

func getNodeImagesFromLocal(localDownloadPath string) (*NodeImages, error) {
// Read the node-images.yaml file from the release
nodeImagePath := filepath.Join(localDownloadPath, nodeImagesFileName)
f, err := os.ReadFile(filepath.Clean(nodeImagePath))
if err != nil {
return nil, fmt.Errorf("failed to read node-images file %s: %w", nodeImagePath, err)
}
nodeImages := NodeImages{}
// if unmarshal fails, it indicates incomplete node-images file.
// But we don't want to enforce download again.
if err = yaml.Unmarshal(f, &nodeImages); err != nil {
return nil, fmt.Errorf("failed to unmarshal node-images: %w", err)
}
return &nodeImages, nil
}

// TODO: Ensure RFC 1123 compatibility.
func ensureMaxNameLength(base string) string {
if len(base) > maxNameLength {
return base[:maxNameLength]
}
return base
}

// SetupWithManager sets up the controller with the Manager.
func (r *OpenStackClusterStackReleaseReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
Expand Down
Loading

0 comments on commit 7571463

Please sign in to comment.