Skip to content

Commit 6b46630

Browse files
authored
feat: Add DSC eval configuration support (#553)
1 parent 757099f commit 6b46630

File tree

5 files changed

+599
-0
lines changed

5 files changed

+599
-0
lines changed

controllers/dsc/config.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package dsc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/go-logr/logr"
9+
corev1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/types"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
)
14+
15+
const (
16+
// DSCConfigMapName is the name of the ConfigMap created by OpenDataHub operator
17+
DSCConfigMapName = "trustyai-dsc-config"
18+
19+
// DSC Configuration keys
20+
DSCPermitOnlineKey = "eval.lmeval.permitOnline"
21+
DSCPermitCodeExecutionKey = "eval.lmeval.permitCodeExecution"
22+
)
23+
24+
// DSCConfig represents the configuration values from the DSC ConfigMap
25+
type DSCConfig struct {
26+
AllowOnline bool
27+
AllowCodeExecution bool
28+
}
29+
30+
// DSCConfigReader reads configuration from the DSC ConfigMap created by OpenDataHub operator
31+
type DSCConfigReader struct {
32+
Client client.Client
33+
Namespace string
34+
}
35+
36+
// ReadDSCConfig reads configuration values from the trustyai-dsc-config ConfigMap
37+
func (r *DSCConfigReader) ReadDSCConfig(ctx context.Context, log *logr.Logger) (*DSCConfig, error) {
38+
if r.Namespace == "" {
39+
log.V(1).Info("No namespace specified, skipping DSC config reading")
40+
return &DSCConfig{}, nil
41+
}
42+
43+
configMapKey := types.NamespacedName{
44+
Namespace: r.Namespace,
45+
Name: DSCConfigMapName,
46+
}
47+
48+
var cm corev1.ConfigMap
49+
if err := r.Client.Get(ctx, configMapKey, &cm); err != nil {
50+
if errors.IsNotFound(err) {
51+
log.V(1).Info("DSC ConfigMap not found, using default configuration",
52+
"configmap", configMapKey)
53+
return &DSCConfig{}, nil
54+
}
55+
return nil, fmt.Errorf("error reading DSC ConfigMap %s: %w", configMapKey, err)
56+
}
57+
58+
log.V(1).Info("Found DSC ConfigMap, processing configuration",
59+
"configmap", configMapKey)
60+
61+
// Process DSC-specific configuration
62+
config, err := r.processDSCConfig(&cm, log)
63+
if err != nil {
64+
return nil, fmt.Errorf("error processing DSC configuration: %w", err)
65+
}
66+
67+
return config, nil
68+
}
69+
70+
// processDSCConfig processes the DSC ConfigMap data and returns configuration
71+
func (r *DSCConfigReader) processDSCConfig(cm *corev1.ConfigMap, log *logr.Logger) (*DSCConfig, error) {
72+
config := &DSCConfig{}
73+
var msgs []string
74+
75+
// Process permitOnline setting
76+
if permitOnlineStr, found := cm.Data[DSCPermitOnlineKey]; found {
77+
permitOnline, err := strconv.ParseBool(permitOnlineStr)
78+
if err != nil {
79+
msgs = append(msgs, fmt.Sprintf("invalid DSC setting for %s: %s, using default",
80+
DSCPermitOnlineKey, permitOnlineStr))
81+
} else {
82+
config.AllowOnline = permitOnline
83+
log.V(1).Info("Read PermitOnline from DSC config",
84+
"key", DSCPermitOnlineKey, "value", permitOnline)
85+
}
86+
}
87+
88+
// Process permitCodeExecution setting
89+
if permitCodeExecutionStr, found := cm.Data[DSCPermitCodeExecutionKey]; found {
90+
permitCodeExecution, err := strconv.ParseBool(permitCodeExecutionStr)
91+
if err != nil {
92+
msgs = append(msgs, fmt.Sprintf("invalid DSC setting for %s: %s, using default",
93+
DSCPermitCodeExecutionKey, permitCodeExecutionStr))
94+
} else {
95+
config.AllowCodeExecution = permitCodeExecution
96+
log.V(1).Info("Read PermitCodeExecution from DSC config",
97+
"key", DSCPermitCodeExecutionKey, "value", permitCodeExecution)
98+
}
99+
}
100+
101+
if len(msgs) > 0 && log != nil {
102+
log.Error(fmt.Errorf("some DSC settings are invalid"), fmt.Sprintf("DSC config errors: %v", msgs))
103+
}
104+
105+
return config, nil
106+
}
107+
108+
// NewDSCConfigReader creates a new DSCConfigReader instance
109+
func NewDSCConfigReader(client client.Client, namespace string) *DSCConfigReader {
110+
return &DSCConfigReader{
111+
Client: client,
112+
Namespace: namespace,
113+
}
114+
}

controllers/dsc/config_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package dsc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/go-logr/logr"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
corev1 "k8s.io/api/core/v1"
12+
"k8s.io/apimachinery/pkg/api/errors"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
17+
)
18+
19+
func TestDSCConfigReader_ReadDSCConfig(t *testing.T) {
20+
tests := []struct {
21+
name string
22+
namespace string
23+
configMapData map[string]string
24+
expectError bool
25+
expectedOnline bool
26+
expectedCode bool
27+
}{
28+
{
29+
name: "ConfigMap not found - should use defaults",
30+
namespace: "test-namespace",
31+
configMapData: nil,
32+
expectError: false,
33+
expectedOnline: false, // Default value
34+
expectedCode: false, // Default value
35+
},
36+
{
37+
name: "Valid configuration with both settings enabled",
38+
namespace: "test-namespace",
39+
configMapData: map[string]string{
40+
DSCPermitOnlineKey: "true",
41+
DSCPermitCodeExecutionKey: "true",
42+
},
43+
expectError: false,
44+
expectedOnline: true,
45+
expectedCode: true,
46+
},
47+
{
48+
name: "Valid configuration with both settings disabled",
49+
namespace: "test-namespace",
50+
configMapData: map[string]string{
51+
DSCPermitOnlineKey: "false",
52+
DSCPermitCodeExecutionKey: "false",
53+
},
54+
expectError: false,
55+
expectedOnline: false,
56+
expectedCode: false,
57+
},
58+
{
59+
name: "Partial configuration - only permitOnline",
60+
namespace: "test-namespace",
61+
configMapData: map[string]string{
62+
DSCPermitOnlineKey: "true",
63+
},
64+
expectError: false,
65+
expectedOnline: true,
66+
expectedCode: false, // Should remain default
67+
},
68+
{
69+
name: "Invalid boolean values - should use defaults",
70+
namespace: "test-namespace",
71+
configMapData: map[string]string{
72+
DSCPermitOnlineKey: "invalid",
73+
DSCPermitCodeExecutionKey: "also-invalid",
74+
},
75+
expectError: false, // Should not fail, just log error
76+
expectedOnline: false, // Should remain default
77+
expectedCode: false, // Should remain default
78+
},
79+
{
80+
name: "Empty namespace - should skip reading",
81+
namespace: "",
82+
configMapData: nil,
83+
expectError: false,
84+
expectedOnline: false, // Should remain default
85+
expectedCode: false, // Should remain default
86+
},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
// Create fake client
92+
var objects []client.Object
93+
if tt.configMapData != nil {
94+
configMap := &corev1.ConfigMap{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Name: DSCConfigMapName,
97+
Namespace: tt.namespace,
98+
},
99+
Data: tt.configMapData,
100+
}
101+
objects = append(objects, configMap)
102+
}
103+
104+
scheme := runtime.NewScheme()
105+
require.NoError(t, corev1.AddToScheme(scheme))
106+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
107+
108+
// Create DSCConfigReader
109+
reader := NewDSCConfigReader(fakeClient, tt.namespace)
110+
111+
// Create a test logger
112+
log := logr.Discard()
113+
114+
// Test ReadDSCConfig
115+
dscConfig, err := reader.ReadDSCConfig(context.Background(), &log)
116+
117+
// Assert error expectations
118+
if tt.expectError {
119+
assert.Error(t, err)
120+
} else {
121+
assert.NoError(t, err)
122+
}
123+
124+
// Assert configuration values
125+
assert.Equal(t, tt.expectedOnline, dscConfig.AllowOnline, "AllowOnline should match expected value")
126+
assert.Equal(t, tt.expectedCode, dscConfig.AllowCodeExecution, "AllowCodeExecution should match expected value")
127+
})
128+
}
129+
}
130+
131+
func TestDSCConfigReader_ReadDSCConfig_ConfigMapNotFound(t *testing.T) {
132+
// Create fake client without any ConfigMaps
133+
scheme := runtime.NewScheme()
134+
require.NoError(t, corev1.AddToScheme(scheme))
135+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
136+
137+
// Create DSCConfigReader
138+
reader := NewDSCConfigReader(fakeClient, "test-namespace")
139+
140+
// Create a test logger
141+
log := logr.Discard()
142+
143+
// Test ReadDSCConfig
144+
dscConfig, err := reader.ReadDSCConfig(context.Background(), &log)
145+
146+
// Should not return error when ConfigMap is not found
147+
assert.NoError(t, err)
148+
149+
// Values should be at defaults
150+
assert.False(t, dscConfig.AllowOnline)
151+
assert.False(t, dscConfig.AllowCodeExecution)
152+
}
153+
154+
func TestDSCConfigReader_ReadDSCConfig_ClientError(t *testing.T) {
155+
// Create a mock client that returns an error
156+
mockClient := &mockClient{shouldError: true}
157+
158+
// Create DSCConfigReader
159+
reader := NewDSCConfigReader(mockClient, "test-namespace")
160+
161+
// Create a test logger
162+
log := logr.Discard()
163+
164+
// Test ReadDSCConfig
165+
_, err := reader.ReadDSCConfig(context.Background(), &log)
166+
167+
// Should return error when client fails
168+
assert.Error(t, err)
169+
assert.Contains(t, err.Error(), "error reading DSC ConfigMap")
170+
}
171+
172+
// mockClient is a simple mock that can simulate client errors
173+
type mockClient struct {
174+
client.Client
175+
shouldError bool
176+
}
177+
178+
func (m *mockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
179+
if m.shouldError {
180+
return errors.NewInternalError(fmt.Errorf("mock client error"))
181+
}
182+
return nil
183+
}
184+
185+
func TestNewDSCConfigReader(t *testing.T) {
186+
// Create a fake client
187+
scheme := runtime.NewScheme()
188+
require.NoError(t, corev1.AddToScheme(scheme))
189+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
190+
191+
// Test NewDSCConfigReader
192+
reader := NewDSCConfigReader(fakeClient, "test-namespace")
193+
194+
// Assert reader is properly initialised
195+
assert.NotNil(t, reader)
196+
assert.Equal(t, fakeClient, reader.Client)
197+
assert.Equal(t, "test-namespace", reader.Namespace)
198+
}

controllers/lmes/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"time"
2525

2626
"github.com/go-logr/logr"
27+
"github.com/trustyai-explainability/trustyai-service-operator/controllers/dsc"
2728
"github.com/trustyai-explainability/trustyai-service-operator/controllers/lmes/driver"
2829
corev1 "k8s.io/api/core/v1"
2930
)
@@ -115,3 +116,11 @@ func constructOptionsFromConfigMap(log *logr.Logger, configmap *corev1.ConfigMap
115116

116117
return nil
117118
}
119+
120+
// ApplyDSCConfig applies DSC configuration to the LMES Options
121+
func ApplyDSCConfig(dscConfig *dsc.DSCConfig) {
122+
if dscConfig != nil {
123+
Options.AllowOnline = dscConfig.AllowOnline
124+
Options.AllowCodeExecution = dscConfig.AllowCodeExecution
125+
}
126+
}

0 commit comments

Comments
 (0)