Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add OpenStackNodeImageRelease creation logic in OpenStackClusterStackRelease controller #25

Merged
merged 1 commit into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading