Drop initial code

This commit is contained in:
Danny Bessems
2026-01-15 09:58:01 +00:00
parent 227d957219
commit 1e7c9ba5cb
228 changed files with 19883 additions and 1 deletions

View File

@@ -0,0 +1,134 @@
package builder
import (
"context"
"encoding/json"
"fmt"
"gopkg.in/yaml.v3"
"vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
"vanderlande.com/ittp/appstack/rig-operator/internal/provider"
)
// ChartConfig holds the helm settings extracted from the YAML _defaults
// The Controller needs this to know WHICH chart to fetch.
type ChartConfig struct {
Repo string
Name string
Version string
}
type MasterBuilder struct {
strategy provider.Strategy
baseTemplate []byte
chartConfig ChartConfig
}
func NewMasterBuilder(strategy provider.Strategy, baseTemplate []byte) *MasterBuilder {
b := &MasterBuilder{
strategy: strategy,
baseTemplate: baseTemplate,
// Safe defaults
chartConfig: ChartConfig{
Name: "oci://ghcr.io/rancherfederal/charts/rancher-cluster-templates",
},
}
return b
}
// GetChartConfig returns the chart details found in the template.
func (b *MasterBuilder) GetChartConfig() ChartConfig {
return b.chartConfig
}
// Build orchestrates the values generation process
func (b *MasterBuilder) Build(ctx context.Context, cbp *v1alpha1.ClusterBlueprint, credentialSecret string) (map[string]interface{}, error) {
values := make(map[string]interface{})
if err := yaml.Unmarshal(b.baseTemplate, &values); err != nil {
return nil, fmt.Errorf("failed to unmarshal base template: %w", err)
}
// 1. Extract Chart Config from _defaults (Legacy Logic Ported)
// We do this so the Controller knows what version to install.
if defaults, ok := values["_defaults"].(map[string]interface{}); ok {
if chartCfg, ok := defaults["helmChart"].(map[string]interface{}); ok {
if v, ok := chartCfg["repo"].(string); ok {
b.chartConfig.Repo = v
}
if v, ok := chartCfg["name"].(string); ok {
b.chartConfig.Name = v
}
if v, ok := chartCfg["version"].(string); ok {
b.chartConfig.Version = v
}
}
}
// 2. Generate Node Pools (Delegated to Strategy)
// [DIFFERENCE]: We don't loop here. The Strategy knows how to map CBP -> Provider NodePools.
nodePools, err := b.strategy.GenerateNodePools(ctx, cbp)
if err != nil {
return nil, fmt.Errorf("strategy failed to generate node pools: %w", err)
}
// 3. Get Global Overrides (Delegated to Strategy)
// [DIFFERENCE]: We don't hardcode "cloud_provider_name" here. The Strategy returns it.
overrides, err := b.strategy.GetGlobalOverrides(ctx, cbp, credentialSecret)
if err != nil {
return nil, fmt.Errorf("strategy failed to get global overrides: %w", err)
}
// 4. Inject Logic into the Helm Structure
if clusterMap, ok := values["cluster"].(map[string]interface{}); ok {
clusterMap["name"] = cbp.Name
if configMap, ok := clusterMap["config"].(map[string]interface{}); ok {
configMap["kubernetesVersion"] = cbp.Spec.KubernetesVersion
// Ensure globalConfig exists
if _, ok := configMap["globalConfig"]; !ok {
configMap["globalConfig"] = make(map[string]interface{})
}
globalConfig := configMap["globalConfig"].(map[string]interface{})
// Inject Overrides
for k, v := range overrides {
// A. Handle specific Global Config keys
if k == "cloud_provider_name" || k == "cloud_provider_config" {
globalConfig[k] = v
continue
}
// B. Handle Chart Values (CCM/CSI Addons)
if k == "chartValues" {
if existingChartVals, ok := configMap["chartValues"].(map[string]interface{}); ok {
if newChartVals, ok := v.(map[string]interface{}); ok {
for ck, cv := range newChartVals {
existingChartVals[ck] = cv
}
}
} else {
configMap["chartValues"] = v
}
continue
}
// C. Default: Inject at Root level
values[k] = v
}
}
}
// 5. Inject Node Pools
// We marshal/unmarshal to ensure JSON tags from the Strategy structs are respected
tempJSON, _ := json.Marshal(nodePools)
var cleanNodePools interface{}
_ = json.Unmarshal(tempJSON, &cleanNodePools)
values["nodepools"] = cleanNodePools
// 6. Cleanup internal keys
delete(values, "_defaults")
return values, nil
}

View File

@@ -0,0 +1,291 @@
package controller
import (
"context"
"fmt"
"time"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
rigv1 "vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
"vanderlande.com/ittp/appstack/rig-operator/internal/builder"
"vanderlande.com/ittp/appstack/rig-operator/internal/helm"
"vanderlande.com/ittp/appstack/rig-operator/internal/provider"
"vanderlande.com/ittp/appstack/rig-operator/internal/provider/harvester"
harvesterTemplate "vanderlande.com/ittp/appstack/rig-operator/internal/templates/harvester"
"vanderlande.com/ittp/appstack/rig-operator/internal/provider/vsphere"
vsphereTemplate "vanderlande.com/ittp/appstack/rig-operator/internal/templates/vsphere"
)
const (
rigFinalizer = "rig.appstack.io/finalizer"
)
// ClusterBlueprintReconciler reconciles a ClusterBlueprint object
type ClusterBlueprintReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
// +kubebuilder:rbac:groups=rig.appstack.io,resources=clusterblueprints,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rig.appstack.io,resources=clusterblueprints/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=rig.appstack.io,resources=clusterblueprints/finalizers,verbs=update
// +kubebuilder:rbac:groups=rig.appstack.io,resources=infrablueprints,verbs=get;list;watch
// +kubebuilder:rbac:groups=rig.appstack.io,resources=harvesterblueprints,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
func (r *ClusterBlueprintReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
// 1. Fetch ClusterBlueprint (CBP)
cbp := &rigv1.ClusterBlueprint{}
if err := r.Get(ctx, req.NamespacedName, cbp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. Handle Deletion ... (Same as before)
if !cbp.ObjectMeta.DeletionTimestamp.IsZero() {
return r.handleDelete(ctx, cbp)
}
// 3. Ensure Finalizer ... (Same as before)
if !controllerutil.ContainsFinalizer(cbp, rigFinalizer) {
controllerutil.AddFinalizer(cbp, rigFinalizer)
if err := r.Update(ctx, cbp); err != nil {
return ctrl.Result{}, err
}
}
// 4. Fetch InfraBlueprint (IBP)
ibp := &rigv1.InfraBlueprint{}
if err := r.Get(ctx, types.NamespacedName{Name: cbp.Spec.InfraBlueprintRef, Namespace: cbp.Namespace}, ibp); err != nil {
l.Error(err, "InfraBlueprint not found", "Infra", cbp.Spec.InfraBlueprintRef)
r.updateStatus(ctx, cbp, "PendingInfra", false)
return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
}
// =====================================================================
// 4.5. QUOTA CHECK (The Gatekeeper)
// Only check quota if we are NOT already deployed.
// (Existing clusters keep running even if quota shrinks later)
// =====================================================================
if cbp.Status.Phase != "Deployed" {
if err := r.checkQuota(cbp, ibp); err != nil {
l.Error(err, "Quota Exceeded")
// We stop here! Helm Apply will NOT run.
r.updateStatus(ctx, cbp, "QuotaExceeded", false)
// Requeue slowly to check if resources freed up later
return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
}
}
// 5. Select Strategy based on Infra ProviderRef
var selectedStrategy provider.Strategy
var baseTemplate []byte
var credentialSecret string
switch ibp.Spec.ProviderRef.Kind {
case "HarvesterBlueprint":
// A. Fetch the specific Harvester Config (HBP)
hbp := &rigv1.HarvesterBlueprint{}
hbpName := types.NamespacedName{Name: ibp.Spec.ProviderRef.Name, Namespace: cbp.Namespace}
if err := r.Get(ctx, hbpName, hbp); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to load HarvesterBlueprint: %w", err)
}
// B. Ensure Identity (Mint ServiceAccount/Secret)
idMgr := harvester.NewIdentityManager(r.Client, r.Scheme)
secretName, err := idMgr.Ensure(ctx, cbp, ibp, hbp)
if err != nil {
l.Error(err, "Failed to ensure identity")
r.updateStatus(ctx, cbp, "ProvisioningFailed", false)
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}
credentialSecret = secretName
// C. Load Defaults & Init Strategy
defaults, err := harvesterTemplate.GetDefaults()
if err != nil {
return ctrl.Result{}, err
}
baseTemplate = harvesterTemplate.GetBaseValues()
// [UPDATED] Pass ibp.Spec.RancherURL to the factory
selectedStrategy = harvester.NewStrategy(
hbp,
ibp.Spec.UserData,
ibp.Spec.RancherURL, // <--- Passing the URL here
defaults,
)
case "VsphereBlueprint":
// A. Fetch the specific vSphere Config (VBP)
vbp := &rigv1.VsphereBlueprint{}
vbpName := types.NamespacedName{Name: ibp.Spec.ProviderRef.Name, Namespace: cbp.Namespace}
if err := r.Get(ctx, vbpName, vbp); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to load VsphereBlueprint: %w", err)
}
// B. Load Defaults (CPU/RAM sizing safety nets)
defaults, err := vsphereTemplate.GetDefaults()
if err != nil {
return ctrl.Result{}, err
}
baseTemplate = vsphereTemplate.GetBaseValues()
// C. Init Strategy
// Note: vSphere typically uses the global 'cloudCredentialSecret' defined in InfraBlueprint
// rather than minting dynamic tokens per cluster like Harvester does.
credentialSecret = ibp.Spec.CloudCredentialSecret
selectedStrategy = vsphere.NewStrategy(
vbp,
ibp.Spec.UserData,
ibp.Spec.RancherURL,
defaults,
)
default:
return ctrl.Result{}, fmt.Errorf("unsupported provider kind: %s", ibp.Spec.ProviderRef.Kind)
}
// 6. Build Helm Values (Generic Engine)
masterBuilder := builder.NewMasterBuilder(selectedStrategy, baseTemplate)
values, err := masterBuilder.Build(ctx, cbp, credentialSecret)
if err != nil {
l.Error(err, "Failed to build helm values")
r.updateStatus(ctx, cbp, "ConfigGenerationFailed", false)
return ctrl.Result{}, nil // Fatal error, don't retry until config changes
}
// 7. Apply Helm Chart
// We use the ChartConfig extracted by the MasterBuilder (from the YAML defaults)
chartCfg := masterBuilder.GetChartConfig()
helmConfig := helm.Config{
Namespace: cbp.Namespace,
ReleaseName: cbp.Name, // We use the Cluster name as the Release name
RepoURL: chartCfg.Repo,
ChartName: chartCfg.Name,
Version: chartCfg.Version,
Values: values,
}
l.Info("Applying Helm Release", "Release", cbp.Name, "Chart", chartCfg.Name)
if err := helm.Apply(helmConfig); err != nil {
l.Error(err, "Helm Install/Upgrade failed")
r.updateStatus(ctx, cbp, "HelmApplyFailed", false)
return ctrl.Result{RequeueAfter: 1 * time.Minute}, nil
}
// 8. Success!
r.updateStatus(ctx, cbp, "Deployed", true)
return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil // Re-sync periodically
}
func (r *ClusterBlueprintReconciler) handleDelete(ctx context.Context, cbp *rigv1.ClusterBlueprint) (ctrl.Result, error) {
if controllerutil.ContainsFinalizer(cbp, rigFinalizer) {
// 1. Uninstall Helm Release
helmCfg := helm.Config{
Namespace: cbp.Namespace,
ReleaseName: cbp.Name,
}
// Best effort uninstall
if err := helm.Uninstall(helmCfg); err != nil {
log.FromContext(ctx).Error(err, "Failed to uninstall helm release during cleanup")
}
// 2. Cleanup Identity (Harvester SA)
// We need to look up IBP -> HBP again to know WHERE to clean up
// This is a simplified lookup; in production we might need to handle missing IBP gracefully
ibp := &rigv1.InfraBlueprint{}
if err := r.Get(ctx, types.NamespacedName{Name: cbp.Spec.InfraBlueprintRef, Namespace: cbp.Namespace}, ibp); err == nil {
if ibp.Spec.ProviderRef.Kind == "HarvesterBlueprint" {
hbp := &rigv1.HarvesterBlueprint{}
if err := r.Get(ctx, types.NamespacedName{Name: ibp.Spec.ProviderRef.Name, Namespace: cbp.Namespace}, hbp); err == nil {
idMgr := harvester.NewIdentityManager(r.Client, r.Scheme)
idMgr.Cleanup(ctx, cbp, ibp, hbp)
}
}
}
// 3. Remove Finalizer
controllerutil.RemoveFinalizer(cbp, rigFinalizer)
if err := r.Update(ctx, cbp); err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *ClusterBlueprintReconciler) updateStatus(ctx context.Context, cbp *rigv1.ClusterBlueprint, phase string, ready bool) {
cbp.Status.Phase = phase
cbp.Status.Ready = ready
if err := r.Status().Update(ctx, cbp); err != nil {
log.FromContext(ctx).Error(err, "Failed to update status")
}
}
// SetupWithManager sets up the controller with the Manager.
func (r *ClusterBlueprintReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&rigv1.ClusterBlueprint{}).
Complete(r)
}
// Helper function to calculate required resources vs available
func (r *ClusterBlueprintReconciler) checkQuota(cbp *rigv1.ClusterBlueprint, ibp *rigv1.InfraBlueprint) error {
// 1. Calculate what this cluster needs
var reqCpu, reqMem, reqDisk int
// Control Plane Sizing (Using safe defaults or template logic)
// Ideally, this should match the defaults in your template/strategy
cpCount := 1
if cbp.Spec.ControlPlaneHA {
cpCount = 3
}
reqCpu += cpCount * 4
reqMem += cpCount * 8
reqDisk += cpCount * 40
// Worker Pools Sizing
for _, pool := range cbp.Spec.WorkerPools {
reqCpu += pool.Quantity * pool.CpuCores
reqMem += pool.Quantity * pool.MemoryGB
reqDisk += pool.Quantity * pool.DiskGB
}
// 2. Check against Limits
// Note: We use the Status.Usage which is calculated by the InfraController.
// This includes "other" clusters, but might include "this" cluster if it was already counted.
// For strict "Admission Control", usually we check:
// (CurrentUsage + Request) > MaxLimit
// However, since InfraController runs asynchronously, 'Status.Usage' might NOT yet include this new cluster.
// So (Usage + Request) > Max is the safest check for a new provisioning.
q := ibp.Spec.Quota
u := ibp.Status.Usage
if q.MaxCPU > 0 && (u.UsedCPU+reqCpu) > q.MaxCPU {
return fmt.Errorf("requested CPU %d exceeds remaining quota (Max: %d, Used: %d)", reqCpu, q.MaxCPU, u.UsedCPU)
}
if q.MaxMemoryGB > 0 && (u.UsedMemoryGB+reqMem) > q.MaxMemoryGB {
return fmt.Errorf("requested Mem %dGB exceeds remaining quota (Max: %d, Used: %d)", reqMem, q.MaxMemoryGB, u.UsedMemoryGB)
}
if q.MaxDiskGB > 0 && (u.UsedDiskGB+reqDisk) > q.MaxDiskGB {
return fmt.Errorf("requested Disk %dGB exceeds remaining quota (Max: %d, Used: %d)", reqDisk, q.MaxDiskGB, u.UsedDiskGB)
}
return nil
}

View File

@@ -0,0 +1,84 @@
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
rigv1alpha1 "vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
)
var _ = Describe("ClusterBlueprint Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
clusterblueprint := &rigv1alpha1.ClusterBlueprint{}
BeforeEach(func() {
By("creating the custom resource for the Kind ClusterBlueprint")
err := k8sClient.Get(ctx, typeNamespacedName, clusterblueprint)
if err != nil && errors.IsNotFound(err) {
resource := &rigv1alpha1.ClusterBlueprint{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &rigv1alpha1.ClusterBlueprint{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance ClusterBlueprint")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &ClusterBlueprintReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})

View File

@@ -0,0 +1,128 @@
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
rigv1 "vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
)
// InfraBlueprintReconciler reconciles a InfraBlueprint object
type InfraBlueprintReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=rig.appstack.io,resources=infrablueprints,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rig.appstack.io,resources=infrablueprints/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=rig.appstack.io,resources=clusterblueprints,verbs=get;list;watch
func (r *InfraBlueprintReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
l := log.FromContext(ctx)
// 1. Fetch the InfraBlueprint
infra := &rigv1.InfraBlueprint{}
if err := r.Get(ctx, req.NamespacedName, infra); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. List ALL ClusterBlueprints in the same namespace
// (We assume Infra and Clusters live in the same namespace for security/tenancy)
var clusterList rigv1.ClusterBlueprintList
if err := r.List(ctx, &clusterList, client.InNamespace(req.Namespace)); err != nil {
l.Error(err, "Failed to list clusters for quota calculation")
return ctrl.Result{}, err
}
// 3. Calculate Usage (The Accountant Logic)
var usedCpu, usedMem, usedDisk int
for _, cluster := range clusterList.Items {
// Only count clusters that belong to THIS Infra
if cluster.Spec.InfraBlueprintRef != infra.Name {
continue
}
// Sum Control Plane
if cluster.Spec.ControlPlaneHA {
// Hardcoded fallback or we could duplicate the defaults logic here.
// Ideally, we'd read the templates, but for accounting, safe estimates are usually okay.
// Or better: The Cluster status could report its own "ResourcesConsumed".
// For now, we use the standard defaults we know:
usedCpu += 3 * 4 // 3 nodes * 4 cores
usedMem += 3 * 8 // 3 nodes * 8 GB
usedDisk += 3 * 40 // 3 nodes * 40 GB
} else {
usedCpu += 1 * 4
usedMem += 1 * 8
usedDisk += 1 * 40
}
// Sum Worker Pools
for _, pool := range cluster.Spec.WorkerPools {
usedCpu += pool.Quantity * pool.CpuCores
usedMem += pool.Quantity * pool.MemoryGB
usedDisk += pool.Quantity * pool.DiskGB
}
}
// 4. Update Status if changed
if infra.Status.Usage.UsedCPU != usedCpu ||
infra.Status.Usage.UsedMemoryGB != usedMem ||
infra.Status.Usage.UsedDiskGB != usedDisk {
infra.Status.Usage.UsedCPU = usedCpu
infra.Status.Usage.UsedMemoryGB = usedMem
infra.Status.Usage.UsedDiskGB = usedDisk
l.Info("Updating Infra Quota Usage", "Infra", infra.Name, "CPU", usedCpu, "Mem", usedMem)
if err := r.Status().Update(ctx, infra); err != nil {
return ctrl.Result{}, err
}
}
// 5. Verify Connectivity (Optional)
// We could check if the ProviderRef exists here and set Ready=true
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *InfraBlueprintReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&rigv1.InfraBlueprint{}).
// Watch ClusterBlueprints too!
// If a Cluster is added/modified, we need to Reconcile the Infra it points to.
Watches(
&rigv1.ClusterBlueprint{},
handler.EnqueueRequestsFromMapFunc(r.findInfraForCluster),
).
Complete(r)
}
// findInfraForCluster maps a Cluster change event to a Reconcile request for its parent Infra
func (r *InfraBlueprintReconciler) findInfraForCluster(ctx context.Context, obj client.Object) []reconcile.Request {
cluster, ok := obj.(*rigv1.ClusterBlueprint)
if !ok {
return nil
}
if cluster.Spec.InfraBlueprintRef != "" {
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: cluster.Spec.InfraBlueprintRef,
Namespace: cluster.Namespace,
},
},
}
}
return nil
}

View File

@@ -0,0 +1,84 @@
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
rigv1alpha1 "vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
)
var _ = Describe("InfraBlueprint Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
infrablueprint := &rigv1alpha1.InfraBlueprint{}
BeforeEach(func() {
By("creating the custom resource for the Kind InfraBlueprint")
err := k8sClient.Get(ctx, typeNamespacedName, infrablueprint)
if err != nil && errors.IsNotFound(err) {
resource := &rigv1alpha1.InfraBlueprint{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &rigv1alpha1.InfraBlueprint{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance InfraBlueprint")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &InfraBlueprintReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})

View File

@@ -0,0 +1,116 @@
/*
Copyright 2026.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
rigv1alpha1 "vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var (
ctx context.Context
cancel context.CancelFunc
testEnv *envtest.Environment
cfg *rest.Config
k8sClient client.Client
)
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
ctx, cancel = context.WithCancel(context.TODO())
var err error
err = rigv1alpha1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
// Retrieve the first found binary directory to allow running tests from IDEs
if getFirstFoundEnvTestBinaryDir() != "" {
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
}
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
cancel()
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
basePath := filepath.Join("..", "..", "bin", "k8s")
entries, err := os.ReadDir(basePath)
if err != nil {
logf.Log.Error(err, "Failed to read directory", "path", basePath)
return ""
}
for _, entry := range entries {
if entry.IsDir() {
return filepath.Join(basePath, entry.Name())
}
}
return ""
}

View File

@@ -0,0 +1,126 @@
package helm
import (
"fmt"
"log"
"os"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/registry" // [NEW] Required for OCI
"helm.sh/helm/v3/pkg/storage/driver"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
type Config struct {
Namespace string
ReleaseName string
RepoURL string
ChartName string
Version string
Values map[string]interface{}
}
func Apply(cfg Config) error {
settings := cli.New()
// 1. Initialize Action Config
actionConfig := new(action.Configuration)
getter := genericclioptions.NewConfigFlags(false)
if err := actionConfig.Init(getter, cfg.Namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil {
return fmt.Errorf("failed to init helm config: %w", err)
}
// 2. [NEW] Initialize OCI Registry Client
// This tells Helm how to talk to ghcr.io, docker.io, etc.
registryClient, err := registry.NewClient(
registry.ClientOptDebug(true),
registry.ClientOptEnableCache(true),
registry.ClientOptCredentialsFile(settings.RegistryConfig), // Uses ~/.config/helm/registry/config.json
)
if err != nil {
return fmt.Errorf("failed to init registry client: %w", err)
}
actionConfig.RegistryClient = registryClient
// 3. Setup Install Action
client := action.NewInstall(actionConfig)
client.Version = cfg.Version
client.Namespace = cfg.Namespace
client.ReleaseName = cfg.ReleaseName
client.CreateNamespace = true
if cfg.RepoURL != "" {
client.RepoURL = cfg.RepoURL
}
// 4. Locate Chart (Now supports oci:// because RegistryClient is set)
cp, err := client.ChartPathOptions.LocateChart(cfg.ChartName, settings)
if err != nil {
return fmt.Errorf("failed to locate chart %s: %w", cfg.ChartName, err)
}
chart, err := loader.Load(cp)
if err != nil {
return fmt.Errorf("failed to load chart: %w", err)
}
// 5. Install or Upgrade
histClient := action.NewHistory(actionConfig)
histClient.Max = 1
if _, err := histClient.Run(cfg.ReleaseName); err == driver.ErrReleaseNotFound {
fmt.Printf("Installing OCI Release %s...\n", cfg.ReleaseName)
_, err := client.Run(chart, cfg.Values)
return err
} else if err != nil {
return err
}
fmt.Printf("Upgrading OCI Release %s...\n", cfg.ReleaseName)
upgrade := action.NewUpgrade(actionConfig)
upgrade.Version = cfg.Version
upgrade.Namespace = cfg.Namespace
// Important: Upgrade also needs the RegistryClient, but it shares 'actionConfig'
// so it is already set up.
if cfg.RepoURL != "" {
upgrade.RepoURL = cfg.RepoURL
}
_, err = upgrade.Run(cfg.ReleaseName, chart, cfg.Values)
return err
}
func Uninstall(cfg Config) error {
settings := cli.New()
// 1. Initialize Action Config (Same as Apply)
actionConfig := new(action.Configuration)
getter := genericclioptions.NewConfigFlags(false)
if err := actionConfig.Init(getter, cfg.Namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil {
return fmt.Errorf("failed to init helm config: %w", err)
}
// 2. Initialize OCI Registry Client (Crucial for OCI charts)
registryClient, err := registry.NewClient(
registry.ClientOptDebug(true),
registry.ClientOptEnableCache(true),
registry.ClientOptCredentialsFile(settings.RegistryConfig),
)
if err != nil {
return fmt.Errorf("failed to init registry client: %w", err)
}
actionConfig.RegistryClient = registryClient
// 3. Run Uninstall
client := action.NewUninstall(actionConfig)
// Don't fail if it's already gone
_, err = client.Run(cfg.ReleaseName)
if err != nil && err != driver.ErrReleaseNotFound {
return fmt.Errorf("failed to uninstall release: %w", err)
}
fmt.Printf("✅ Uninstalled Release %s\n", cfg.ReleaseName)
return nil
}

View File

@@ -0,0 +1,176 @@
package harvester
import (
"context"
"encoding/base64"
"fmt"
"time"
authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
// DeleteCredentialResources connects to Harvester and removes the specific SA and bindings
func DeleteCredentialResources(ctx context.Context, masterKubeconfig []byte, serviceAccountName, vmNamespace string) error {
restConfig, err := clientcmd.RESTConfigFromKubeConfig(masterKubeconfig)
if err != nil {
return err
}
hvClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return err
}
deletePolicy := metav1.DeletePropagationBackground
deleteOpts := metav1.DeleteOptions{PropagationPolicy: &deletePolicy}
// 1. Delete Global CSI Binding (ClusterRoleBinding)
csiBindingName := fmt.Sprintf("%s-csi-binding", serviceAccountName)
// We ignore NotFound errors to make this idempotent
if err := hvClient.RbacV1().ClusterRoleBindings().Delete(ctx, csiBindingName, deleteOpts); err != nil && !apierrors.IsNotFound(err) {
return err
}
// 2. Delete Cloud Provider Binding (RoleBinding in VM Namespace)
cpBindingName := fmt.Sprintf("%s-cloud-binding", serviceAccountName)
if err := hvClient.RbacV1().RoleBindings(vmNamespace).Delete(ctx, cpBindingName, deleteOpts); err != nil && !apierrors.IsNotFound(err) {
return err
}
// 3. Delete ServiceAccount (VM Namespace)
if err := hvClient.CoreV1().ServiceAccounts(vmNamespace).Delete(ctx, serviceAccountName, deleteOpts); err != nil && !apierrors.IsNotFound(err) {
return err
}
return nil
}
// EnsureCredential mints a dedicated ServiceAccount in the specific VM Namespace
func EnsureCredential(ctx context.Context, masterKubeconfig []byte, clusterName, targetNamespace, vmNamespace, harvesterURL string) (*corev1.Secret, string, time.Time, error) {
// --- PHASE 1: Connect ---
restConfig, err := clientcmd.RESTConfigFromKubeConfig(masterKubeconfig)
if err != nil {
return nil, "", time.Time{}, fmt.Errorf("invalid rancher cloud credential kubeconfig: %w", err)
}
hvClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, "", time.Time{}, err
}
// --- PHASE 2: Create Identity ---
if vmNamespace == "" {
vmNamespace = "default"
}
saName := fmt.Sprintf("prov-%s", clusterName)
// A. Create ServiceAccount
sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: saName, Namespace: vmNamespace}}
if _, err := hvClient.CoreV1().ServiceAccounts(vmNamespace).Create(ctx, sa, metav1.CreateOptions{}); err != nil {
if !apierrors.IsAlreadyExists(err) {
return nil, "", time.Time{}, err
}
}
// B. Create RoleBinding (Cloud Provider)
rb := &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: saName + "-cloud-binding", Namespace: vmNamespace},
Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: saName, Namespace: vmNamespace}},
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", Name: "harvesterhci.io:cloudprovider", APIGroup: "rbac.authorization.k8s.io"},
}
if _, err := hvClient.RbacV1().RoleBindings(vmNamespace).Create(ctx, rb, metav1.CreateOptions{}); err != nil {
if !apierrors.IsAlreadyExists(err) { /* Ignore */
}
}
// C. Create ClusterRoleBinding (CSI Driver)
crb := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{Name: saName + "-csi-binding"},
Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: saName, Namespace: vmNamespace}},
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", Name: "harvesterhci.io:csi-driver", APIGroup: "rbac.authorization.k8s.io"},
}
if _, err := hvClient.RbacV1().ClusterRoleBindings().Create(ctx, crb, metav1.CreateOptions{}); err != nil {
if !apierrors.IsAlreadyExists(err) { /* Ignore */
}
}
// D. Mint Token
ttlSeconds := int64(315360000) // ~10 years
tokenRequest, err := hvClient.CoreV1().ServiceAccounts(vmNamespace).CreateToken(ctx, saName, &authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: &ttlSeconds},
}, metav1.CreateOptions{})
if err != nil {
return nil, "", time.Time{}, fmt.Errorf("failed to mint harvester token: %w", err)
}
expiryTime := time.Now().Add(time.Duration(ttlSeconds) * time.Second)
// --- PHASE 3: Determine URL & CA ---
if harvesterURL == "" {
harvesterURL = restConfig.Host
}
// Fetch internal CA (required because proxy CA != internal CA)
harvesterCA := restConfig.CAData
caConfigMap, err := hvClient.CoreV1().ConfigMaps("default").Get(ctx, "kube-root-ca.crt", metav1.GetOptions{})
if err == nil {
if caStr, ok := caConfigMap.Data["ca.crt"]; ok {
harvesterCA = []byte(caStr)
}
}
// --- PHASE 4: Construct Kubeconfig ---
caData := base64.StdEncoding.EncodeToString(harvesterCA)
token := tokenRequest.Status.Token
newKubeconfig := fmt.Sprintf(
`apiVersion: v1
kind: Config
clusters:
- name: harvester
cluster:
server: %s
certificate-authority-data: %s
users:
- name: provisioner
user:
token: %s
contexts:
- name: default
context:
cluster: harvester
user: provisioner
namespace: %s
current-context: default
`, harvesterURL, caData, token, vmNamespace)
// --- PHASE 5: Create Secret Object ---
secretName := fmt.Sprintf("harvesterconfig-%s", clusterName)
secret := &corev1.Secret{
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: targetNamespace,
Annotations: map[string]string{
// [CRITICAL] These annotations authorize the guest cluster to use this secret
"v2prov-secret-authorized-for-cluster": clusterName,
"v2prov-authorized-secret-deletes-on-cluster-removal": "true",
},
Labels: map[string]string{
"cattle.io/creator": "rig-operator", // Updated creator
"rig.appstack.io/cluster": clusterName,
},
},
Type: "Opaque",
StringData: map[string]string{
"credential": newKubeconfig,
},
}
return secret, saName, expiryTime, nil
}

View File

@@ -0,0 +1,126 @@
package harvester
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
)
type IdentityManager struct {
client client.Client
scheme *runtime.Scheme
}
func NewIdentityManager(c client.Client, s *runtime.Scheme) *IdentityManager {
return &IdentityManager{client: c, scheme: s}
}
// Ensure checks if an identity exists. If not, it fetches master creds, mints a new one, and updates Status.
func (m *IdentityManager) Ensure(ctx context.Context, cbp *v1alpha1.ClusterBlueprint, ibp *v1alpha1.InfraBlueprint, hbp *v1alpha1.HarvesterBlueprint) (string, error) {
l := log.FromContext(ctx)
// 1. Fast Path: If identity already exists in Status, return it
if cbp.Status.Identity != nil && cbp.Status.Identity.SecretRef != "" {
return cbp.Status.Identity.SecretRef, nil
}
l.Info("Minting Harvester identity", "Cluster", cbp.Name)
// 2. Fetch Master Credential (from Infra)
rancherCredName := ibp.Spec.CloudCredentialSecret
if rancherCredName == "" {
return "", fmt.Errorf("CloudCredentialSecret is missing in InfraBlueprint %s", ibp.Name)
}
var rancherSecret corev1.Secret
// Note: Rancher secrets are expected in cattle-global-data
if err := m.client.Get(ctx, types.NamespacedName{Name: rancherCredName, Namespace: "cattle-global-data"}, &rancherSecret); err != nil {
return "", fmt.Errorf("failed to fetch rancher credential %s: %w", rancherCredName, err)
}
// 3. Extract Kubeconfig
const kubeconfigKey = "harvestercredentialConfig-kubeconfigContent"
adminKubeconfigBytes := rancherSecret.Data[kubeconfigKey]
if len(adminKubeconfigBytes) == 0 {
if len(rancherSecret.Data["credential"]) > 0 {
adminKubeconfigBytes = rancherSecret.Data["credential"]
} else {
return "", fmt.Errorf("secret %s missing kubeconfig data", rancherCredName)
}
}
// 4. Call Factory (low-level)
newSecret, saName, _, err := EnsureCredential(
ctx,
adminKubeconfigBytes,
cbp.Name,
cbp.Namespace, // Target Namespace (where secret goes)
hbp.Spec.VmNamespace, // Harvester Namespace (where VM goes)
hbp.Spec.HarvesterURL, // Explicit URL from HBP
)
if err != nil {
return "", fmt.Errorf("failed to mint harvester credential: %w", err)
}
// 5. Persist Secret
// Set OwnerRef so if CBP is deleted, Secret is deleted automatically
if err := controllerutil.SetControllerReference(cbp, newSecret, m.scheme); err != nil {
return "", err
}
patchOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner("rig-operator")}
if err := m.client.Patch(ctx, newSecret, client.Apply, patchOpts...); err != nil {
return "", fmt.Errorf("failed to patch new secret: %w", err)
}
// 6. Update CBP Status
// We do this here so the identity is "locked" to the object immediately
if cbp.Status.Identity == nil {
cbp.Status.Identity = &v1alpha1.IdentityStatus{}
}
cbp.Status.Identity.SecretRef = newSecret.Name
cbp.Status.Identity.ServiceAccount = saName
if err := m.client.Status().Update(ctx, cbp); err != nil {
return "", fmt.Errorf("failed to update cluster status: %w", err)
}
return newSecret.Name, nil
}
// Cleanup removes the ServiceAccount from Harvester when the Cluster is deleted
func (m *IdentityManager) Cleanup(ctx context.Context, cbp *v1alpha1.ClusterBlueprint, ibp *v1alpha1.InfraBlueprint, hbp *v1alpha1.HarvesterBlueprint) {
if cbp.Status.Identity == nil || cbp.Status.Identity.ServiceAccount == "" {
return
}
// Fetch Master Secret again to get connection details
rancherCredName := ibp.Spec.CloudCredentialSecret
var rancherSecret corev1.Secret
if err := m.client.Get(ctx, types.NamespacedName{Name: rancherCredName, Namespace: "cattle-global-data"}, &rancherSecret); err != nil {
log.FromContext(ctx).V(1).Info("Cleanup: Could not fetch master secret (connection lost), skipping manual cleanup")
return
}
var kubeBytes []byte
if len(rancherSecret.Data["harvestercredentialConfig-kubeconfigContent"]) > 0 {
kubeBytes = rancherSecret.Data["harvestercredentialConfig-kubeconfigContent"]
} else if len(rancherSecret.Data["credential"]) > 0 {
kubeBytes = rancherSecret.Data["credential"]
} else {
return
}
// Delegate to low-level cleanup
if err := DeleteCredentialResources(ctx, kubeBytes, cbp.Status.Identity.ServiceAccount, hbp.Spec.VmNamespace); err != nil {
log.FromContext(ctx).Error(err, "Failed to cleanup Harvester resources (best effort)")
}
}

View File

@@ -0,0 +1,140 @@
package harvester
import (
"context"
"fmt"
"vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
template "vanderlande.com/ittp/appstack/rig-operator/internal/templates/harvester"
)
// harvesterNodePool matches the exact JSON structure required by the Helm Chart
type harvesterNodePool struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
Quantity int `json:"quantity"`
Etcd bool `json:"etcd"`
ControlPlane bool `json:"controlplane"`
Worker bool `json:"worker"`
Paused bool `json:"paused"`
// Harvester Specific Fields
CpuCount int `json:"cpuCount"`
MemorySize int `json:"memorySize"` // GB
DiskSize int `json:"diskSize"` // GB
ImageName string `json:"imageName"`
NetworkName string `json:"networkName"`
SshUser string `json:"sshUser"`
VmNamespace string `json:"vmNamespace"`
UserData string `json:"userData"`
}
type Strategy struct {
blueprint *v1alpha1.HarvesterBlueprint
userData string
rancherURL string
defaults template.Defaults
}
// NewStrategy initializes the strategy with defaults and optional overrides
func NewStrategy(hbp *v1alpha1.HarvesterBlueprint, infraUserData string, infraRancherURL string, defaults template.Defaults) *Strategy { // 1. Determine UserData priority: Infra (IBP) > Template Default
finalUserData := infraUserData
if finalUserData == "" {
finalUserData = defaults.UserData
}
return &Strategy{
blueprint: hbp,
userData: finalUserData,
rancherURL: infraRancherURL,
defaults: defaults,
}
}
// GenerateNodePools implements provider.Strategy
func (s *Strategy) GenerateNodePools(ctx context.Context, cbp *v1alpha1.ClusterBlueprint) (interface{}, error) {
var pools []interface{}
// Helper to map generic req -> harvester specific struct
mapPool := func(name string, qty, cpu, memGB, diskGB int, isEtcd, isCp, isWk bool) harvesterNodePool {
return harvesterNodePool{
Name: name,
DisplayName: name,
Quantity: qty,
Etcd: isEtcd,
ControlPlane: isCp,
Worker: isWk,
Paused: false,
// Mapping: Generic (GB) -> Harvester (GB) [No conversion needed]
CpuCount: cpu,
MemorySize: memGB,
DiskSize: diskGB,
// Harvester Specifics from HBP
ImageName: s.blueprint.Spec.ImageName,
NetworkName: s.blueprint.Spec.NetworkName,
SshUser: s.blueprint.Spec.SshUser,
VmNamespace: s.blueprint.Spec.VmNamespace,
UserData: s.userData,
}
}
// 1. Control Plane Pool
cpQty := 1
if cbp.Spec.ControlPlaneHA {
cpQty = 3
}
// Use Defaults from YAML for CP sizing
pools = append(pools, mapPool(
"cp-pool",
cpQty,
s.defaults.CP_CPU,
s.defaults.CP_Mem,
s.defaults.CP_Disk,
true, true, false,
))
// 2. Worker Pools
for _, wp := range cbp.Spec.WorkerPools {
pools = append(pools, mapPool(
wp.Name,
wp.Quantity,
wp.CpuCores,
wp.MemoryGB,
wp.DiskGB,
false, false, true,
))
}
return pools, nil
}
// GetGlobalOverrides implements provider.Strategy
func (s *Strategy) GetGlobalOverrides(ctx context.Context, cbp *v1alpha1.ClusterBlueprint, credentialSecretName string) (map[string]interface{}, error) {
// secret://<namespace>:<secretName>
secretURI := fmt.Sprintf("secret://%s:%s", cbp.Namespace, credentialSecretName)
overrides := map[string]interface{}{
"cloud_provider_name": "harvester",
"cloud_provider_config": secretURI,
// Inject Rancher URL
"rancher": map[string]interface{}{
"cattle": map[string]interface{}{
"url": s.rancherURL,
},
},
"chartValues": map[string]interface{}{
"harvester-cloud-provider": map[string]interface{}{
"global": map[string]interface{}{
"cattle": map[string]interface{}{
"clusterName": cbp.Name,
},
},
},
},
}
return overrides, nil
}

View File

@@ -0,0 +1,16 @@
package provider
import (
"context"
"vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
)
type Strategy interface {
// GenerateNodePools generates the provider-specific node pool list.
// [CHANGED] Return type is now interface{} to support both Structs and Maps
GenerateNodePools(ctx context.Context, cbp *v1alpha1.ClusterBlueprint) (interface{}, error)
// GetGlobalOverrides returns the provider-specific helm values.
GetGlobalOverrides(ctx context.Context, cbp *v1alpha1.ClusterBlueprint, credentialSecret string) (map[string]interface{}, error)
}

View File

@@ -0,0 +1,138 @@
package vsphere
import (
"context"
rigv1 "vanderlande.com/ittp/appstack/rig-operator/api/v1alpha1"
vspheretpl "vanderlande.com/ittp/appstack/rig-operator/internal/templates/vsphere"
)
type Strategy struct {
blueprint *rigv1.VsphereBlueprint
userData string
rancherURL string
defaults vspheretpl.Defaults
}
// NewStrategy creates the vSphere logic handler
func NewStrategy(vbp *rigv1.VsphereBlueprint, userData string, rancherURL string, defaults vspheretpl.Defaults) *Strategy {
// 1. Resolve UserData (Infra > Template Default)
finalUserData := userData
if finalUserData == "" {
finalUserData = defaults.UserData
}
return &Strategy{
blueprint: vbp,
userData: finalUserData,
rancherURL: rancherURL,
defaults: defaults,
}
}
// GenerateNodePools maps the generic ClusterBlueprint to vSphere-specific NodePool maps
func (s *Strategy) GenerateNodePools(ctx context.Context, cbp *rigv1.ClusterBlueprint) (interface{}, error) {
var nodePools []map[string]interface{}
// 1. Control Plane Node Pool
// We rely on the defaults extracted from values.yaml (e.g. 4 Core, 8GB)
// vSphere Chart expects MB, so we multiply GB * 1024.
cpQty := 1
if cbp.Spec.ControlPlaneHA {
cpQty = 3
}
nodePools = append(nodePools, s.buildPool(
"control-plane-nodes", // Name
"cp-nodes", // Display Name
cpQty, // Quantity
s.defaults.CP_CPU, // Cores
s.defaults.CP_Mem*1024, // RAM (GB -> MB)
s.defaults.CP_Disk*1024, // Disk (GB -> MB)
true, // Etcd
true, // ControlPlane
false, // Worker
))
// 2. Worker Pools
// We iterate over the user's requested pools in the CBP
for _, wp := range cbp.Spec.WorkerPools {
nodePools = append(nodePools, s.buildPool(
wp.Name,
wp.Name,
wp.Quantity,
wp.CpuCores,
wp.MemoryGB*1024, // Convert GB to MB
wp.DiskGB*1024, // Convert GB to MB
false, // Etcd
false, // ControlPlane
true, // Worker
))
}
return nodePools, nil
}
// GetGlobalOverrides injects the vSphere-specific global values (Cloud Provider, Credentials, URLs)
func (s *Strategy) GetGlobalOverrides(ctx context.Context, cbp *rigv1.ClusterBlueprint, credentialSecret string) (map[string]interface{}, error) {
overrides := map[string]interface{}{
// Tell Helm we are on vSphere
"cloudprovider": "vsphere",
// The Secret containing username/password/vcenter-address
"cloudCredentialSecretName": credentialSecret,
// Register with the correct Rancher Manager
"rancher": map[string]interface{}{
"cattle": map[string]interface{}{
"url": s.rancherURL,
},
},
// Cluster Metadata
"cluster": map[string]interface{}{
"name": cbp.Name,
"config": map[string]interface{}{
"kubernetesVersion": cbp.Spec.KubernetesVersion,
},
},
}
return overrides, nil
}
// buildPool is a private helper to construct the exact map structure the vSphere Helm Chart expects
func (s *Strategy) buildPool(name, displayName string, qty, cpu, ramMB, diskMB int, etcd, cp, worker bool) map[string]interface{} {
pool := map[string]interface{}{
// Generic RKE2 Node Settings
"name": name,
"displayName": displayName,
"quantity": qty,
"etcd": etcd,
"controlplane": cp,
"worker": worker,
"paused": false,
// vSphere Infrastructure Location (From Blueprint)
"vcenter": s.blueprint.Spec.VCenter,
"datacenter": s.blueprint.Spec.Datacenter,
"folder": s.blueprint.Spec.Folder,
"pool": s.blueprint.Spec.ResourcePool,
"datastoreCluster": s.blueprint.Spec.Datastore, // Assumes chart supports this key. If not, use "datastore".
"network": []string{s.blueprint.Spec.Network},
// Cloning Details
"creationType": "template",
"cloneFrom": s.blueprint.Spec.Template,
// Hardware Sizing (Already converted to MB)
"cpuCount": cpu,
"memorySize": ramMB,
"diskSize": diskMB,
// Cloud Init
"cloudConfig": s.userData,
}
return pool
}

View File

@@ -0,0 +1,69 @@
package harvester
import (
_ "embed"
"fmt"
"gopkg.in/yaml.v3"
)
//go:embed values.yaml
var valuesYAML []byte
type Defaults struct {
CP_CPU int
CP_Mem int
CP_Disk int
ChartRepo string
ChartName string
ChartVersion string
// [NEW] Default UserData for this provider
UserData string
}
func GetDefaults() (Defaults, error) {
var raw map[string]interface{}
if err := yaml.Unmarshal(valuesYAML, &raw); err != nil {
return Defaults{}, fmt.Errorf("failed to parse harvester base values: %w", err)
}
d := Defaults{
CP_CPU: 4, CP_Mem: 8, CP_Disk: 40, // Safety Fallbacks
}
if defs, ok := raw["_defaults"].(map[string]interface{}); ok {
if cp, ok := defs["controlPlaneProfile"].(map[string]interface{}); ok {
if v, ok := cp["cpuCores"].(int); ok {
d.CP_CPU = v
}
if v, ok := cp["memoryGb"].(int); ok {
d.CP_Mem = v
}
if v, ok := cp["diskGb"].(int); ok {
d.CP_Disk = v
}
}
if chart, ok := defs["helmChart"].(map[string]interface{}); ok {
if v, ok := chart["repo"].(string); ok {
d.ChartRepo = v
}
if v, ok := chart["name"].(string); ok {
d.ChartName = v
}
if v, ok := chart["version"].(string); ok {
d.ChartVersion = v
}
}
// [NEW] Extract UserData
if v, ok := defs["userData"].(string); ok {
d.UserData = v
}
}
return d, nil
}
func GetBaseValues() []byte {
return valuesYAML
}

View File

@@ -0,0 +1,456 @@
# ----------------------------------------------------------------
# BASE TEMPLATE (internal/templates/base_values.yaml)
# ----------------------------------------------------------------
_defaults:
helmChart:
repo: ""
name: "oci://ghcr.io/rancherfederal/charts/rancher-cluster-templates"
version: "0.7.2"
controlPlaneProfile:
cpuCores: 4
memoryGb: 8
diskGb: 40
userData: &userData |
#cloud-config
package_update: false
package_upgrade: false
snap:
commands:
00: snap refresh --hold=forever
package_reboot_if_required: true
packages:
- qemu-guest-agent
- yq
- jq
- curl
- wget
bootcmd:
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv6.conf.default.disable_ipv6=1
write_files:
# ----------------------------------------------------------------
# 1. CNI Permission Fix Script & Cron (CIS 1.1.9 Persistence)
# ----------------------------------------------------------------
- path: /usr/local/bin/fix-cni-perms.sh
permissions: '0700'
owner: root:root
content: |
#!/bin/bash
# Wait 60s on boot for RKE2 to write files
[ "$1" == "boot" ] && sleep 60
# Enforce 600 on CNI files (CIS 1.1.9)
if [ -d /etc/cni/net.d ]; then
find /etc/cni/net.d -type f -exec chmod 600 {} \;
fi
if [ -d /var/lib/cni/networks ]; then
find /var/lib/cni/networks -type f -exec chmod 600 {} \;
fi
# Every RKE2 service restart can reset CNI file permissions, so we run
# this script on reboot and daily via cron to maintain CIS compliance.
- path: /etc/cron.d/cis-cni-fix
permissions: '0644'
owner: root:root
content: |
# Run on Reboot (with delay) to fix files created during startup
@reboot root /usr/local/bin/fix-cni-perms.sh boot
# Run once daily at 00:00 to correct any drift
0 0 * * * root /usr/local/bin/fix-cni-perms.sh
# ----------------------------------------------------------------
# 2. RKE2 Admission Config
# ----------------------------------------------------------------
- path: /etc/rancher/rke2/rke2-admission.yaml
permissions: '0600'
owner: root:root
content: |
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
defaults:
enforce: "restricted"
enforce-version: "latest"
audit: "restricted"
audit-version: "latest"
warn: "restricted"
warn-version: "latest"
exemptions:
usernames: []
runtimeClasses: []
namespaces: [compliance-operator-system,kube-system, cis-operator-system, tigera-operator, calico-system, rke2-ingress-nginx, cattle-system, cattle-fleet-system, longhorn-system, cattle-neuvector-system]
- name: EventRateLimit
configuration:
apiVersion: eventratelimit.admission.k8s.io/v1alpha1
kind: Configuration
limits:
- type: Server
qps: 5000
burst: 20000
# ----------------------------------------------------------------
# 3. RKE2 Audit Policy
# ----------------------------------------------------------------
- path: /etc/rancher/rke2/audit-policy.yaml
permissions: '0600'
owner: root:root
content: |
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: None
users: ["system:kube-controller-manager", "system:kube-scheduler", "system:serviceaccount:kube-system:endpoint-controller"]
verbs: ["get", "update"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status"]
- level: None
verbs: ["get"]
resources:
- group: ""
resources: ["nodes", "nodes/status", "pods", "pods/status"]
- level: None
users: ["kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status", "configmaps"]
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: RequestResponse
omitStages:
- RequestReceived
# ----------------------------------------------------------------
# 4. Static NetworkPolicies
# ----------------------------------------------------------------
- path: /var/lib/rancher/rke2/server/manifests/cis-network-policy.yaml
permissions: '0600'
owner: root:root
content: |
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-metrics
namespace: kube-public
spec:
podSelector: {}
ingress:
- {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-system
namespace: kube-system
spec:
podSelector: {}
ingress:
- {}
policyTypes:
- Ingress
# ----------------------------------------------------------------
# 5. Service Account Hardening
# ----------------------------------------------------------------
- path: /var/lib/rancher/rke2/server/manifests/cis-sa-config.yaml
permissions: '0600'
owner: root:root
content: |
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: default
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: kube-system
automountServiceAccountToken: false
- path: /var/lib/rancher/rke2/server/manifests/cis-sa-cron.yaml
permissions: '0600'
owner: root:root
content: |
apiVersion: v1
kind: ServiceAccount
metadata: {name: sa-cleaner, namespace: kube-system}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata: {name: sa-cleaner-role}
rules:
- apiGroups: [""]
resources: ["namespaces", "serviceaccounts"]
verbs: ["get", "list", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: {name: sa-cleaner-binding}
subjects: [{kind: ServiceAccount, name: sa-cleaner, namespace: kube-system}]
roleRef: {kind: ClusterRole, name: sa-cleaner-role, apiGroup: rbac.authorization.k8s.io}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: sa-cleaner
namespace: kube-system
spec:
schedule: "0 */6 * * *" # Run every 6 hours
jobTemplate:
spec:
template:
spec:
serviceAccountName: sa-cleaner
containers:
- name: cleaner
image: rancher/kubectl:v1.26.0
command:
- /bin/bash
- -c
- |
# Get all namespaces
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
# Check if default SA has automount=true (or null)
automount=$(kubectl get sa default -n $ns -o jsonpath='{.automountServiceAccountToken}')
if [ "$automount" != "false" ]; then
echo "Securing default SA in namespace: $ns"
kubectl patch sa default -n $ns -p '{"automountServiceAccountToken": false}'
fi
done
restartPolicy: OnFailure
# ----------------------------------------------------------------
# 6. OS Sysctls Hardening
# ----------------------------------------------------------------
- path: /etc/sysctl.d/60-rke2-cis.conf
permissions: '0644'
content: |
vm.overcommit_memory=1
vm.max_map_count=65530
vm.panic_on_oom=0
fs.inotify.max_user_watches=1048576
fs.inotify.max_user_instances=8192
kernel.panic=10
kernel.panic_on_oops=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.default.log_martians=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.icmp_ignore_bogus_error_responses=1
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
fs.protected_hardlinks=1
fs.protected_symlinks=1
# ----------------------------------------------------------------
# 7. Environment & Setup Scripts
# ----------------------------------------------------------------
- path: /etc/profile.d/rke2.sh
permissions: '0644'
content: |
export PATH=$PATH:/var/lib/rancher/rke2/bin:/opt/rke2/bin
export KUBECONFIG=/etc/rancher/rke2/rke2.yaml
- path: /root/updates.sh
permissions: '0550'
content: |
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
apt-mark hold linux-headers-generic
apt-mark hold linux-headers-virtual
apt-mark hold linux-image-virtual
apt-mark hold linux-virtual
apt-get update
apt-get upgrade -y
apt-get autoremove -y
users:
- name: rancher
gecos: Rancher service account
hashed_passwd: $6$Mas.x2i7B2cefjUy$59363FmEuoU.LiTLNRZmtemlH2W0D0SWsig22KSZ3QzOmfxeZXxdSx5wIw9wO7GXF/M9W.9SHoKVBOYj1HPX3.
lock_passwd: false
shell: /bin/bash
groups: [users, sudo, docker]
sudo: ALL=(ALL:ALL) ALL
ssh_authorized_keys:
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEwWnnOTAu0LlAZRczQ0Z0KvNlUdPhGQhpZie+nF1O3s'
- name: etcd
gecos: "etcd user"
shell: /sbin/nologin
system: true
lock_passwd: true
disable_root: true
ssh_pwauth: true
runcmd:
- systemctl enable --now qemu-guest-agent
- sysctl --system
- /root/updates.sh
# Immediate run of fix script
- /usr/local/bin/fix-cni-perms.sh
final_message: |
VI_CNV_CLOUD_INIT has been applied successfully.
Node ready for Rancher!
# amazonec2, azure, digitalocean, harvester, vsphere, custom
cloudprovider: harvester
# cloud provider credentials
cloudCredentialSecretName: cc-mrklm
# rancher manager url
rancher:
cattle:
url: rancher-mgmt.product.lan
# cluster values
cluster:
name: default-cluster
# labels:
# key: value
config:
kubernetesVersion: v1.33.5+rke2r1
enableNetworkPolicy: true
localClusterAuthEndpoint:
enabled: false
chartValues:
harvester-cloud-provider:
global:
cattle:
clusterName: default-cluster
# Pod Security Standard (Replaces PSP)
defaultPodSecurityAdmissionConfigurationTemplateName: "rancher-restricted"
globalConfig:
systemDefaultRegistry: docker.io
cni: canal
docker: false
disable_scheduler: false
disable_cloud_controller: false
disable_kube_proxy: false
etcd_expose_metrics: false
profile: 'cis'
selinux: false
secrets_encryption: true
write_kubeconfig_mode: 0600
use_service_account_credentials: false
protect_kernel_defaults: true
cloud_provider_name: harvester
cloud_provider_config: secret://fleet-default:harvesterconfigzswmd
kube_apiserver_arg:
- "service-account-extend-token-expiration=false"
- "anonymous-auth=false"
- "enable-admission-plugins=NodeRestriction,PodSecurity,EventRateLimit,DenyServiceExternalIPs"
- "admission-control-config-file=/etc/rancher/rke2/rke2-admission.yaml"
- "audit-policy-file=/etc/rancher/rke2/audit-policy.yaml"
- "audit-log-path=/var/lib/rancher/rke2/server/logs/audit.log"
- "audit-log-maxage=30"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=100"
kubelet_arg:
# Strong Ciphers (CIS 4.2.12)
- "tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
# PID Limit (CIS 4.2.13)
- "pod-max-pids=4096"
# Seccomp Default (CIS 4.2.14)
- "seccomp-default=true"
- "protect-kernel-defaults=true"
- "make-iptables-util-chains=true"
upgradeStrategy:
controlPlaneConcurrency: 10%
controlPlaneDrainOptions:
enabled: false
workerConcurrency: 10%
workerDrainOptions:
enabled: false
addons:
monitoring:
enabled: false
logging:
enabled: false
longhorn:
enabled: false
neuvector:
enabled: false
# node and nodepool(s) values
# ----------------------------------------------------------------
# MANUAL TESTING SECTION
# The Operator will DELETE and OVERWRITE this section at runtime.
# These values are only used if you run 'helm install' manually.
# ----------------------------------------------------------------
nodepools:
- name: control-plane-nodes
displayName: cp-nodes
quantity: 1
etcd: true
controlplane: true
worker: false
paused: false
cpuCount: 4
diskSize: 40
imageName: vanderlande/image-qhtpc
memorySize: 8
networkName: vanderlande/vm-lan
sshUser: rancher
vmNamespace: vanderlande
userData: *userData
- name: worker-nodes
displayName: wk-nodes
quantity: 2
etcd: false
controlplane: false
worker: true
paused: false
cpuCount: 2
diskSize: 40
imageName: vanderlande/image-qmx5q
memorySize: 8
networkName: vanderlande/vm-lan
sshUser: rancher
vmNamespace: vanderlande
userData: *userData

View File

@@ -0,0 +1,77 @@
package vsphere
import (
_ "embed"
"fmt"
"gopkg.in/yaml.v3"
)
//go:embed values.yaml
var valuesYAML []byte
type Defaults struct {
CP_CPU int
CP_Mem int
CP_Disk int
ChartRepo string
ChartName string
ChartVersion string
UserData string
}
// GetDefaults parses the embedded values.yaml to extract global settings
func GetDefaults() (Defaults, error) {
var raw map[string]interface{}
if err := yaml.Unmarshal(valuesYAML, &raw); err != nil {
return Defaults{}, fmt.Errorf("failed to parse vsphere base values: %w", err)
}
// 1. Set Hardcoded Fallbacks (Safety Net)
d := Defaults{
CP_CPU: 2, CP_Mem: 4, CP_Disk: 40, // vSphere might need different defaults than Harvester
}
// 2. Read from _defaults block
if defs, ok := raw["_defaults"].(map[string]interface{}); ok {
// Profile Defaults
if cp, ok := defs["controlPlaneProfile"].(map[string]interface{}); ok {
if v, ok := cp["cpuCores"].(int); ok {
d.CP_CPU = v
}
if v, ok := cp["memoryGb"].(int); ok {
d.CP_Mem = v
}
if v, ok := cp["diskGb"].(int); ok {
d.CP_Disk = v
}
}
// Helm Chart Defaults
if chart, ok := defs["helmChart"].(map[string]interface{}); ok {
if v, ok := chart["repo"].(string); ok {
d.ChartRepo = v
}
if v, ok := chart["name"].(string); ok {
d.ChartName = v
}
if v, ok := chart["version"].(string); ok {
d.ChartVersion = v
}
}
// UserData Default
if v, ok := defs["userData"].(string); ok {
d.UserData = v
}
}
return d, nil
}
// GetBaseValues returns the raw bytes for the MasterBuilder
func GetBaseValues() []byte {
return valuesYAML
}

View File

@@ -0,0 +1,202 @@
# ----------------------------------------------------------------
# BASE TEMPLATE (internal/templates/base_values.yaml)
# ----------------------------------------------------------------
_defaults:
helmChart:
repo: ""
name: "oci://ghcr.io/rancherfederal/charts/rancher-cluster-templates"
version: "0.7.2"
controlPlaneProfile:
cpuCores: 4
memoryGb: 8
diskGb: 40
userData: &userData |
#cloud-config
package_update: false
package_upgrade: false
snap:
commands:
00: snap refresh --hold=forever
package_reboot_if_required: true
packages:
- yq
- jq
disable_root: true
ssh_pwauth: false
write_files:
- path: /root/updates.sh
permissions: '0550'
content: |
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
apt-mark hold linux-headers-generic
apt-mark hold linux-headers-virtual
apt-mark hold linux-image-virtual
apt-mark hold linux-virtual
apt-get update
apt-get upgrade -y
apt-get autoremove -y
users:
- name: rancher
gecos: Rancher service account
hashed_passwd: $6$Mas.x2i7B2cefjUy$59363FmEuoU.LiTLNRZmtemlH2W0D0SWsig22KSZ3QzOmfxeZXxdSx5wIw9wO7GXF/M9W.9SHoKVBOYj1HPX3.
lock_passwd: false
shell: /bin/bash
groups: [users, sudo, docker]
sudo: ALL=(ALL:ALL) ALL
ssh_authorized_keys:
- 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEwWnnOTAu0LlAZRczQ0Z0KvNlUdPhGQhpZie+nF1O3s'
disable_root: true
ssh_pwauth: true
runcmd:
# - systemctl enable --now qemu-guest-agent
- sysctl --system
- /root/updates.sh
# Immediate run of fix script
bootcmd:
- sudo bash /root/networking.sh
final_message: |
VI_CNV_CLOUD_INIT has been applied successfully.
Node ready for Rancher!
# amazonec2, azure, digitalocean, harvester, vsphere, custom
cloudprovider: vsphere
# cloud provider credentials
cloudCredentialSecretName: cc-lhtl9
# rancher manager url
rancher:
cattle:
url: rancher.tst.vanderlande.com
# cluster values
cluster:
name: default-cluster-005
# labels:
# key: value
config:
kubernetesVersion: v1.31.12+rke2r1
enableNetworkPolicy: true
localClusterAuthEndpoint:
enabled: false
# Pod Security Standard (Replaces PSP)
# defaultPodSecurityAdmissionConfigurationTemplateName: "rancher-restricted"
globalConfig:
systemDefaultRegistry: docker.io
cni: canal
docker: false
disable_scheduler: false
disable_cloud_controller: false
disable_kube_proxy: false
etcd_expose_metrics: false
profile: ''
selinux: false
secrets_encryption: false
write_kubeconfig_mode: 0600
use_service_account_credentials: false
protect_kernel_defaults: false
cloud_provider_name: ''
# kube_apiserver_arg:
# - "service-account-extend-token-expiration=false"
# - "anonymous-auth=false"
# - "enable-admission-plugins=NodeRestriction,PodSecurity,EventRateLimit,DenyServiceExternalIPs"
# - "admission-control-config-file=/etc/rancher/rke2/rke2-admission.yaml"
# - "audit-policy-file=/etc/rancher/rke2/audit-policy.yaml"
# - "audit-log-path=/var/lib/rancher/rke2/server/logs/audit.log"
# - "audit-log-maxage=30"
# - "audit-log-maxbackup=10"
# - "audit-log-maxsize=100"
# kubelet_arg:
# # Strong Ciphers (CIS 4.2.12)
# - "tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"
# # PID Limit (CIS 4.2.13)
# - "pod-max-pids=4096"
# # Seccomp Default (CIS 4.2.14)
# - "seccomp-default=true"
# - "protect-kernel-defaults=true"
# - "make-iptables-util-chains=true"
upgradeStrategy:
controlPlaneConcurrency: 10%
controlPlaneDrainOptions:
enabled: false
workerConcurrency: 10%
workerDrainOptions:
enabled: false
addons:
monitoring:
enabled: false
logging:
enabled: false
longhorn:
enabled: true
neuvector:
enabled: false
# node and nodepool(s) values
# ----------------------------------------------------------------
# MANUAL TESTING SECTION
# The Operator will DELETE and OVERWRITE this section at runtime.
# These values are only used if you run 'helm install' manually.
# ----------------------------------------------------------------
nodepools:
- name: control-plane-nodes
displayName: cp-nodes
quantity: 1
etcd: true
controlplane: true
worker: false
paused: false
# VSPHERE SPECIFIC FIELDS
cpuCount: 2
memorySize: 8192
diskSize: 40000
vcenter: "vcenter.vanderlande.com"
datacenter: "NL001"
folder: "ICT Digitalisation - Rancher"
pool: "NL001 Development - Rancher/Resources"
datastoreCluster: "NL001 Development - Rancher SDRS" # Matches your SDRS input
network:
- "nl001.vDS.Distri.Vlan.1542"
# Provisioning Source
creationType: "template"
cloneFrom: "nl001-cp-ubuntu-22.04-amd64-20250327-5.15.0-135-rke2-k3s"
cloudConfig: *userData # Using the anchor from your base file
- name: worker-storage-nodes
displayName: wk-nodes
quantity: 2
etcd: false
controlplane: false
worker: true
paused: false
# VSPHERE SPECIFIC FIELDS
cpuCount: 4
memorySize: 8192
diskSize: 100000
vcenter: "vcenter.vanderlande.com"
datacenter: "NL001"
folder: "ICT Digitalisation - Rancher"
pool: "NL001 Development - Rancher/Resources"
datastoreCluster: "NL001 Development - Rancher SDRS" # Matches your SDRS input
network:
- "nl001.vDS.Distri.Vlan.1542"
# Provisioning Source
creationType: "template"
cloneFrom: "nl001-cp-ubuntu-22.04-amd64-20250327-5.15.0-135-rke2-k3s"
cloudConfig: *userData # Using the anchor from your base file