Skip to content

Commit 0e459af

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. - Add ImageMappingOverrides field to LlamaStackDistributionReconciler - Implement parseImageMappingOverrides() to read image-overrides from ConfigMap - Support symbolic name mapping (e.g., `starter`) 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 <[email protected]>
1 parent 238b5b6 commit 0e459af

File tree

7 files changed

+456
-52
lines changed

7 files changed

+456
-52
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,35 @@ kubectl apply -f feature-flags.yaml
134134
Within the next reconciliation loop the operator will begin creating a `<name>-network-policy` resource for each distribution.
135135
Set `enabled: false` (or remove the block) to turn the feature back off; the operator will delete the previously managed policies.
136136

137+
## Image Mapping Overrides
138+
139+
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.
140+
141+
### Configuration
142+
143+
Create or update the operator ConfigMap with an `image-overrides` key:
144+
145+
```yaml
146+
147+
image-overrides: |
148+
starter-gpu: quay.io/custom/llama-stack:starter-gpu
149+
starter: quay.io/custom/llama-stack:starter
150+
```
151+
152+
### Configuration Format
153+
154+
Use the distribution name directly as the key (e.g., `starter-gpu`, `starter`). The operator will apply these overrides automatically
155+
156+
### Example Usage
157+
158+
To update the LLS Distribution image for all `starter` distributions:
159+
160+
```bash
161+
kubectl patch configmap llama-stack-operator-config -n llama-stack-k8s-operator-system --type merge -p '{"data":{"image-overrides":"starter: quay.io/opendatahub/llama-stack:latest"}}'
162+
```
163+
164+
This will cause all LlamaStackDistribution resources using the `starter` distribution to restart with the new image.
165+
137166
## Developer Guide
138167

139168
### Prerequisites

controllers/llamastackdistribution_controller.go

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333

3434
"github.com/go-logr/logr"
3535
"github.com/google/go-cmp/cmp"
36+
"github.com/google/go-containerregistry/pkg/name"
3637
llamav1alpha1 "github.com/llamastack/llama-stack-k8s-operator/api/v1alpha1"
3738
"github.com/llamastack/llama-stack-k8s-operator/pkg/cluster"
3839
"github.com/llamastack/llama-stack-k8s-operator/pkg/deploy"
@@ -94,6 +95,8 @@ type LlamaStackDistributionReconciler struct {
9495
Scheme *runtime.Scheme
9596
// Feature flags
9697
EnableNetworkPolicy bool
98+
// Image mapping overrides
99+
ImageMappingOverrides map[string]string
97100
// Cluster info
98101
ClusterInfo *cluster.ClusterInfo
99102
httpClient *http.Client
@@ -682,21 +685,40 @@ func (r *LlamaStackDistributionReconciler) configMapUpdatePredicate(e event.Upda
682685
return false
683686
}
684687

685-
// Parse the feature flags if the operator config ConfigMap has changed
688+
// Check if this is the operator config ConfigMap
689+
if r.handleOperatorConfigUpdate(newConfigMap) {
690+
return true
691+
}
692+
693+
// Handle referenced ConfigMap updates
694+
return r.handleReferencedConfigMapUpdate(oldConfigMap, newConfigMap)
695+
}
696+
697+
// handleOperatorConfigUpdate processes updates to the operator config ConfigMap.
698+
func (r *LlamaStackDistributionReconciler) handleOperatorConfigUpdate(configMap *corev1.ConfigMap) bool {
686699
operatorNamespace, err := deploy.GetOperatorNamespace()
687700
if err != nil {
688701
return false
689702
}
690-
if newConfigMap.Name == operatorConfigData && newConfigMap.Namespace == operatorNamespace {
691-
EnableNetworkPolicy, err := parseFeatureFlags(newConfigMap.Data)
692-
if err != nil {
693-
log.FromContext(context.Background()).Error(err, "Failed to parse feature flags")
694-
} else {
695-
r.EnableNetworkPolicy = EnableNetworkPolicy
696-
}
697-
return true
703+
704+
if configMap.Name != operatorConfigData || configMap.Namespace != operatorNamespace {
705+
return false
698706
}
699707

708+
// Update feature flags
709+
EnableNetworkPolicy, err := parseFeatureFlags(configMap.Data)
710+
if err != nil {
711+
log.FromContext(context.Background()).Error(err, "Failed to parse feature flags")
712+
} else {
713+
r.EnableNetworkPolicy = EnableNetworkPolicy
714+
}
715+
716+
r.ImageMappingOverrides = ParseImageMappingOverrides(context.Background(), configMap.Data)
717+
return true
718+
}
719+
720+
// handleReferencedConfigMapUpdate processes updates to referenced ConfigMaps.
721+
func (r *LlamaStackDistributionReconciler) handleReferencedConfigMapUpdate(oldConfigMap, newConfigMap *corev1.ConfigMap) bool {
700722
// Only proceed if this ConfigMap is referenced by any LlamaStackDistribution
701723
if !r.isConfigMapReferenced(newConfigMap) {
702724
return false
@@ -868,7 +890,7 @@ func (r *LlamaStackDistributionReconciler) findLlamaStackDistributionsForConfigM
868890

869891
operatorNamespace, err := deploy.GetOperatorNamespace()
870892
if err != nil {
871-
log.FromContext(context.Background()).Error(err, "Failed to get operator namespace for config map event processing")
893+
logger.Error(err, "Failed to get operator namespace for config map event processing")
872894
return nil
873895
}
874896
// If the operator config was changed, we reconcile all LlamaStackDistributions
@@ -1753,53 +1775,103 @@ func NewLlamaStackDistributionReconciler(ctx context.Context, client client.Clie
17531775
return nil, fmt.Errorf("failed to get operator namespace: %w", err)
17541776
}
17551777

1756-
// Get the ConfigMap
1757-
// If the ConfigMap doesn't exist, create it with default feature flags
1758-
// If the ConfigMap exists, parse the feature flags from the Configmap
1778+
// Initialize operator config ConfigMap
1779+
configMap, err := initializeOperatorConfigMap(ctx, client, operatorNamespace)
1780+
if err != nil {
1781+
return nil, err
1782+
}
1783+
1784+
// Parse feature flags from ConfigMap
1785+
enableNetworkPolicy, err := parseFeatureFlags(configMap.Data)
1786+
if err != nil {
1787+
return nil, fmt.Errorf("failed to parse feature flags: %w", err)
1788+
}
1789+
1790+
// Parse image mapping overrides from ConfigMap
1791+
imageMappingOverrides := ParseImageMappingOverrides(ctx, configMap.Data)
1792+
1793+
return &LlamaStackDistributionReconciler{
1794+
Client: client,
1795+
Scheme: scheme,
1796+
EnableNetworkPolicy: enableNetworkPolicy,
1797+
ImageMappingOverrides: imageMappingOverrides,
1798+
ClusterInfo: clusterInfo,
1799+
httpClient: &http.Client{Timeout: 5 * time.Second},
1800+
}, nil
1801+
}
1802+
1803+
// initializeOperatorConfigMap gets or creates the operator config ConfigMap.
1804+
func initializeOperatorConfigMap(ctx context.Context, c client.Client, operatorNamespace string) (*corev1.ConfigMap, error) {
17591805
configMap := &corev1.ConfigMap{}
17601806
configMapName := types.NamespacedName{
17611807
Name: operatorConfigData,
17621808
Namespace: operatorNamespace,
17631809
}
17641810

1765-
if err = client.Get(ctx, configMapName, configMap); err != nil {
1766-
if !k8serrors.IsNotFound(err) {
1767-
return nil, fmt.Errorf("failed to get ConfigMap: %w", err)
1768-
}
1811+
err := c.Get(ctx, configMapName, configMap)
1812+
if err == nil {
1813+
return configMap, nil
1814+
}
17691815

1770-
// ConfigMap doesn't exist, create it with defaults
1771-
configMap, err = createDefaultConfigMap(configMapName)
1772-
if err != nil {
1773-
return nil, fmt.Errorf("failed to generate default configMap: %w", err)
1816+
if !k8serrors.IsNotFound(err) {
1817+
return nil, fmt.Errorf("failed to get ConfigMap: %w", err)
1818+
}
1819+
1820+
// ConfigMap doesn't exist, create it with defaults
1821+
configMap, err = createDefaultConfigMap(configMapName)
1822+
if err != nil {
1823+
return nil, fmt.Errorf("failed to generate default configMap: %w", err)
1824+
}
1825+
1826+
if err = c.Create(ctx, configMap); err != nil {
1827+
return nil, fmt.Errorf("failed to create ConfigMap: %w", err)
1828+
}
1829+
1830+
return configMap, nil
1831+
}
1832+
1833+
func ParseImageMappingOverrides(ctx context.Context, configMapData map[string]string) map[string]string {
1834+
imageMappingOverrides := make(map[string]string)
1835+
logger := log.FromContext(ctx)
1836+
1837+
// Look for the image-overrides key in the ConfigMap data
1838+
if overridesYAML, exists := configMapData["image-overrides"]; exists {
1839+
// Parse the YAML content
1840+
var overrides map[string]string
1841+
if err := yaml.Unmarshal([]byte(overridesYAML), &overrides); err != nil {
1842+
// Log error but continue with empty overrides
1843+
logger.V(1).Info("failed to parse image-overrides YAML", "error", err)
1844+
return imageMappingOverrides
17741845
}
17751846

1776-
if err = client.Create(ctx, configMap); err != nil {
1777-
return nil, fmt.Errorf("failed to create ConfigMap: %w", err)
1847+
// Validate and copy the parsed overrides to our result map
1848+
for version, image := range overrides {
1849+
// Validate the image reference format
1850+
if _, err := name.ParseReference(image); err != nil {
1851+
logger.V(1).Info(
1852+
"skipping invalid image override",
1853+
"version", version,
1854+
"image", image,
1855+
"error", err,
1856+
)
1857+
continue
1858+
}
1859+
imageMappingOverrides[version] = image
17781860
}
17791861
}
17801862

1781-
// Parse feature flags from ConfigMap
1782-
enableNetworkPolicy, err := parseFeatureFlags(configMap.Data)
1783-
if err != nil {
1784-
return nil, fmt.Errorf("failed to parse feature flags: %w", err)
1785-
}
1786-
return &LlamaStackDistributionReconciler{
1787-
Client: client,
1788-
Scheme: scheme,
1789-
EnableNetworkPolicy: enableNetworkPolicy,
1790-
ClusterInfo: clusterInfo,
1791-
httpClient: &http.Client{Timeout: 5 * time.Second},
1792-
}, nil
1863+
return imageMappingOverrides
17931864
}
17941865

17951866
// NewTestReconciler creates a reconciler for testing, allowing injection of a custom http client and feature flags.
17961867
func NewTestReconciler(client client.Client, scheme *runtime.Scheme, clusterInfo *cluster.ClusterInfo,
17971868
httpClient *http.Client, enableNetworkPolicy bool) *LlamaStackDistributionReconciler {
17981869
return &LlamaStackDistributionReconciler{
1799-
Client: client,
1800-
Scheme: scheme,
1801-
ClusterInfo: clusterInfo,
1802-
httpClient: httpClient,
1803-
EnableNetworkPolicy: enableNetworkPolicy,
1870+
Client: client,
1871+
Scheme: scheme,
1872+
ClusterInfo: clusterInfo,
1873+
httpClient: httpClient,
1874+
EnableNetworkPolicy: enableNetworkPolicy,
1875+
ImageMappingOverrides: make(map[string]string),
18041876
}
18051877
}

0 commit comments

Comments
 (0)