Skip to content

Commit 677ecac

Browse files
committed
feat: ConfigMap image mapping overrides for LLS Distro
Implements a mechanism for the Llama Stack Operator to read and apply LLS Distribution image updates from a ConfigMap, enabling independent patching for security fixes or bug fixes without requiring a new LLS Operator or RHOAI version. - Add RHODS version detection from ClusterServiceVersion - Add ImageMappingOverrides field to LlamaStackDistributionReconciler - Implement parseImageMappingOverrides() to read image-overrides from ConfigMap - Add RBAC permissions for operators.coreos.com/clusterserviceversions - Support symbolic name mapping (e.g., rhdev-2.25) to specific images - Included unit tests The operator now reads image overrides from the 'image-overrides' key in the operator ConfigMap, supporting YAML format with version-to-image mappings. Overrides take precedence over default distribution images and are refreshed on each reconciler initialization. Closes: RHAIENG-1079 Signed-off-by: Derek Higgins <derekh@redhat.com>
1 parent 9f076ff commit 677ecac

File tree

11 files changed

+964
-217
lines changed

11 files changed

+964
-217
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,47 @@ Example to create a run.yaml ConfigMap, and a LlamaStackDistribution that refere
104104
kubectl apply -f config/samples/example-with-configmap.yaml
105105
```
106106

107+
## Image Mapping Overrides
108+
109+
The operator supports ConfigMap-driven image updates for LLS Distribution images. This allows independent patching for security fixes or bug fixes without requiring a new operator version.
110+
111+
### Configuration
112+
113+
Create or update the operator ConfigMap with an `image-overrides` key:
114+
115+
```yaml
116+
apiVersion: v1
117+
kind: ConfigMap
118+
metadata:
119+
name: llama-stack-operator-config
120+
namespace: llama-stack-k8s-operator-system
121+
data:
122+
image-overrides: |
123+
rh-dev-2.25: quay.io/rhoai/rhoai-fbc-fragment:rhoai-2.25@sha256:3bc98555
124+
```
125+
126+
### Symbolic Name Format
127+
128+
Use the format `rh-dev-<major>-<minor>` for symbolic names that correspond to RHOAI versions. The operator will automatically detect the current RHOAI version and apply the appropriate override.
129+
130+
### How It Works
131+
132+
1. The operator reads image overrides from the `image-overrides` key in the operator ConfigMap
133+
2. Overrides are parsed as YAML with version-to-image mappings
134+
3. When deploying a LlamaStackDistribution, the operator checks for overrides matching the current RHOAI version
135+
4. If an override exists, it uses the specified image instead of the default distribution image
136+
5. Changes to the ConfigMap automatically trigger reconciliation of all LlamaStackDistribution resources
137+
138+
### Example Usage
139+
140+
To update the LLS Distribution image for RHOAI 2.25:
141+
142+
```bash
143+
kubectl patch configmap llama-stack-operator-config -n llama-stack-k8s-operator-system --type merge -p '{"data":{"image-overrides":"rh-dev-2.25: quay.io/opendatahub/llama-stack:latest"}}'
144+
```
145+
146+
This will cause all LlamaStackDistribution resources using the `rh-dev` name to restart with the new image while using RHOAI version 2.25.Z, once RHOAI is upgraded to another version then the distribution will revert back to the default for that version.
147+
107148
## Developer Guide
108149

109150
### Prerequisites

config/crd/bases/llamastack.io_llamastackdistributions.yaml

Lines changed: 197 additions & 33 deletions
Large diffs are not rendered by default.

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ rules:
8787
- patch
8888
- update
8989
- watch
90+
- apiGroups:
91+
- operators.coreos.com
92+
resources:
93+
- clusterserviceversions
94+
verbs:
95+
- get
96+
- list
97+
- watch
9098
- apiGroups:
9199
- rbac.authorization.k8s.io
92100
resources:

controllers/kubebuilder_rbac.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ package controllers
2727

2828
// NetworkPolicy permissions - controller creates and manages network policies
2929
//+kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete
30+
31+
// ClusterServiceVersion permissions - controller reads CSV to get RHOAI version
32+
//+kubebuilder:rbac:groups=operators.coreos.com,resources=clusterserviceversions,verbs=get;list;watch

controllers/llamastackdistribution_controller.go

Lines changed: 129 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/llamastack/llama-stack-k8s-operator/pkg/cluster"
3535
"github.com/llamastack/llama-stack-k8s-operator/pkg/deploy"
3636
"github.com/llamastack/llama-stack-k8s-operator/pkg/featureflags"
37+
olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1"
3738
"gopkg.in/yaml.v3"
3839
appsv1 "k8s.io/api/apps/v1"
3940
corev1 "k8s.io/api/core/v1"
@@ -89,6 +90,10 @@ type LlamaStackDistributionReconciler struct {
8990
Scheme *runtime.Scheme
9091
// Feature flags
9192
EnableNetworkPolicy bool
93+
// RHODS version
94+
RHODSVersion string
95+
// Image mapping overrides
96+
ImageMappingOverrides map[string]string
9297
// Cluster info
9398
ClusterInfo *cluster.ClusterInfo
9499
httpClient *http.Client
@@ -462,21 +467,40 @@ func (r *LlamaStackDistributionReconciler) configMapUpdatePredicate(e event.Upda
462467
return false
463468
}
464469

465-
// Parse the feature flags if the operator config ConfigMap has changed
470+
// Check if this is the operator config ConfigMap
471+
if r.handleOperatorConfigUpdate(newConfigMap) {
472+
return true
473+
}
474+
475+
// Handle referenced ConfigMap updates
476+
return r.handleReferencedConfigMapUpdate(oldConfigMap, newConfigMap)
477+
}
478+
479+
// handleOperatorConfigUpdate processes updates to the operator config ConfigMap.
480+
func (r *LlamaStackDistributionReconciler) handleOperatorConfigUpdate(configMap *corev1.ConfigMap) bool {
466481
operatorNamespace, err := deploy.GetOperatorNamespace()
467482
if err != nil {
468483
return false
469484
}
470-
if newConfigMap.Name == operatorConfigData && newConfigMap.Namespace == operatorNamespace {
471-
EnableNetworkPolicy, err := parseFeatureFlags(newConfigMap.Data)
472-
if err != nil {
473-
log.FromContext(context.Background()).Error(err, "Failed to parse feature flags")
474-
} else {
475-
r.EnableNetworkPolicy = EnableNetworkPolicy
476-
}
477-
return true
485+
486+
if configMap.Name != operatorConfigData || configMap.Namespace != operatorNamespace {
487+
return false
488+
}
489+
490+
// Update feature flags
491+
EnableNetworkPolicy, err := parseFeatureFlags(configMap.Data)
492+
if err != nil {
493+
log.FromContext(context.Background()).Error(err, "Failed to parse feature flags")
494+
} else {
495+
r.EnableNetworkPolicy = EnableNetworkPolicy
478496
}
479497

498+
r.ImageMappingOverrides = ParseImageMappingOverrides(configMap.Data)
499+
return true
500+
}
501+
502+
// handleReferencedConfigMapUpdate processes updates to referenced ConfigMaps.
503+
func (r *LlamaStackDistributionReconciler) handleReferencedConfigMapUpdate(oldConfigMap, newConfigMap *corev1.ConfigMap) bool {
480504
// Only proceed if this ConfigMap is referenced by any LlamaStackDistribution
481505
if !r.isConfigMapReferenced(newConfigMap) {
482506
return false
@@ -648,7 +672,7 @@ func (r *LlamaStackDistributionReconciler) findLlamaStackDistributionsForConfigM
648672

649673
operatorNamespace, err := deploy.GetOperatorNamespace()
650674
if err != nil {
651-
log.FromContext(context.Background()).Error(err, "Failed to get operator namespace for config map event processing")
675+
logger.Error(err, "Failed to get operator namespace for config map event processing")
652676
return nil
653677
}
654678
// If the operator config was changed, we reconcile all LlamaStackDistributions
@@ -1449,53 +1473,119 @@ func NewLlamaStackDistributionReconciler(ctx context.Context, client client.Clie
14491473
return nil, fmt.Errorf("failed to get operator namespace: %w", err)
14501474
}
14511475

1452-
// Get the ConfigMap
1453-
// If the ConfigMap doesn't exist, create it with default feature flags
1454-
// If the ConfigMap exists, parse the feature flags from the Configmap
1476+
// Initialize operator config ConfigMap
1477+
configMap, err := initializeOperatorConfigMap(ctx, client, operatorNamespace)
1478+
if err != nil {
1479+
return nil, err
1480+
}
1481+
1482+
// Detect RHODS version
1483+
rhodsVersion := detectRHODSVersion(ctx, client, operatorNamespace)
1484+
1485+
// Parse feature flags from ConfigMap
1486+
enableNetworkPolicy, err := parseFeatureFlags(configMap.Data)
1487+
if err != nil {
1488+
return nil, fmt.Errorf("failed to parse feature flags: %w", err)
1489+
}
1490+
1491+
// Parse image mapping overrides from ConfigMap
1492+
imageMappingOverrides := ParseImageMappingOverrides(configMap.Data)
1493+
1494+
return &LlamaStackDistributionReconciler{
1495+
Client: client,
1496+
Scheme: scheme,
1497+
EnableNetworkPolicy: enableNetworkPolicy,
1498+
RHODSVersion: rhodsVersion,
1499+
ImageMappingOverrides: imageMappingOverrides,
1500+
ClusterInfo: clusterInfo,
1501+
httpClient: &http.Client{Timeout: 5 * time.Second},
1502+
}, nil
1503+
}
1504+
1505+
// initializeOperatorConfigMap gets or creates the operator config ConfigMap.
1506+
func initializeOperatorConfigMap(ctx context.Context, c client.Client, operatorNamespace string) (*corev1.ConfigMap, error) {
14551507
configMap := &corev1.ConfigMap{}
14561508
configMapName := types.NamespacedName{
14571509
Name: operatorConfigData,
14581510
Namespace: operatorNamespace,
14591511
}
14601512

1461-
if err = client.Get(ctx, configMapName, configMap); err != nil {
1462-
if !k8serrors.IsNotFound(err) {
1463-
return nil, fmt.Errorf("failed to get ConfigMap: %w", err)
1513+
err := c.Get(ctx, configMapName, configMap)
1514+
if err == nil {
1515+
return configMap, nil
1516+
}
1517+
1518+
if !k8serrors.IsNotFound(err) {
1519+
return nil, fmt.Errorf("failed to get ConfigMap: %w", err)
1520+
}
1521+
1522+
// ConfigMap doesn't exist, create it with defaults
1523+
configMap, err = createDefaultConfigMap(configMapName)
1524+
if err != nil {
1525+
return nil, fmt.Errorf("failed to generate default configMap: %w", err)
1526+
}
1527+
1528+
if err = c.Create(ctx, configMap); err != nil {
1529+
return nil, fmt.Errorf("failed to create ConfigMap: %w", err)
1530+
}
1531+
1532+
return configMap, nil
1533+
}
1534+
1535+
// detectRHODSVersion detects the RHODS/RHOAI version from the ClusterServiceVersion.
1536+
func detectRHODSVersion(ctx context.Context, c client.Client, operatorNamespace string) string {
1537+
csvlist := &olmv1alpha1.ClusterServiceVersionList{}
1538+
listOpts := []client.ListOption{
1539+
client.InNamespace(operatorNamespace),
1540+
}
1541+
err := c.List(ctx, csvlist, listOpts...)
1542+
if err != nil {
1543+
fmt.Printf("failed to list ClusterServiceVersions: %v\n", err)
1544+
return ""
1545+
}
1546+
1547+
for _, csv := range csvlist.Items {
1548+
if strings.HasPrefix(csv.Name, "rhods-operator") {
1549+
return csv.Spec.Version.String()
14641550
}
1551+
}
14651552

1466-
// ConfigMap doesn't exist, create it with defaults
1467-
configMap, err = createDefaultConfigMap(configMapName)
1468-
if err != nil {
1469-
return nil, fmt.Errorf("failed to generate default configMap: %w", err)
1553+
fmt.Printf("failed to find RHODS version from ClusterServiceVersion\n")
1554+
return ""
1555+
}
1556+
1557+
func ParseImageMappingOverrides(configMapData map[string]string) map[string]string {
1558+
imageMappingOverrides := make(map[string]string)
1559+
1560+
// Look for the image-overrides key in the ConfigMap data
1561+
if overridesYAML, exists := configMapData["image-overrides"]; exists {
1562+
// Parse the YAML content
1563+
var overrides map[string]string
1564+
if err := yaml.Unmarshal([]byte(overridesYAML), &overrides); err != nil {
1565+
// Log error but continue with empty overrides
1566+
fmt.Printf("failed to parse image-overrides YAML: %v\n", err)
1567+
return imageMappingOverrides
14701568
}
14711569

1472-
if err = client.Create(ctx, configMap); err != nil {
1473-
return nil, fmt.Errorf("failed to create ConfigMap: %w", err)
1570+
// Copy the parsed overrides to our result map
1571+
for version, image := range overrides {
1572+
imageMappingOverrides[version] = image
14741573
}
14751574
}
14761575

1477-
// Parse feature flags from ConfigMap
1478-
enableNetworkPolicy, err := parseFeatureFlags(configMap.Data)
1479-
if err != nil {
1480-
return nil, fmt.Errorf("failed to parse feature flags: %w", err)
1481-
}
1482-
return &LlamaStackDistributionReconciler{
1483-
Client: client,
1484-
Scheme: scheme,
1485-
EnableNetworkPolicy: enableNetworkPolicy,
1486-
ClusterInfo: clusterInfo,
1487-
httpClient: &http.Client{Timeout: 5 * time.Second},
1488-
}, nil
1576+
return imageMappingOverrides
14891577
}
14901578

14911579
// NewTestReconciler creates a reconciler for testing, allowing injection of a custom http client and feature flags.
14921580
func NewTestReconciler(client client.Client, scheme *runtime.Scheme, clusterInfo *cluster.ClusterInfo,
14931581
httpClient *http.Client, enableNetworkPolicy bool) *LlamaStackDistributionReconciler {
14941582
return &LlamaStackDistributionReconciler{
1495-
Client: client,
1496-
Scheme: scheme,
1497-
ClusterInfo: clusterInfo,
1498-
httpClient: httpClient,
1499-
EnableNetworkPolicy: enableNetworkPolicy,
1583+
Client: client,
1584+
Scheme: scheme,
1585+
ClusterInfo: clusterInfo,
1586+
httpClient: httpClient,
1587+
EnableNetworkPolicy: enableNetworkPolicy,
1588+
RHODSVersion: "",
1589+
ImageMappingOverrides: make(map[string]string),
15001590
}
15011591
}

0 commit comments

Comments
 (0)