|
1 | 1 | package render_test |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "cmp" |
5 | 4 | "errors" |
6 | 5 | "fmt" |
7 | | - "os" |
8 | | - "path/filepath" |
9 | 6 | "reflect" |
10 | | - "slices" |
11 | | - "strings" |
12 | 7 | "testing" |
13 | 8 |
|
14 | 9 | "github.com/stretchr/testify/require" |
| 10 | + "gotest.tools/v3/assert" |
15 | 11 | appsv1 "k8s.io/api/apps/v1" |
16 | 12 | corev1 "k8s.io/api/core/v1" |
| 13 | + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| 14 | + "k8s.io/apimachinery/pkg/runtime/schema" |
17 | 15 | "sigs.k8s.io/controller-runtime/pkg/client" |
18 | | - "sigs.k8s.io/yaml" |
19 | 16 |
|
20 | 17 | "github.com/operator-framework/api/pkg/operators/v1alpha1" |
21 | 18 |
|
22 | 19 | "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle" |
23 | | - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" |
24 | 20 | "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render" |
25 | | - "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1" |
26 | 21 | . "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util/testing" |
27 | 22 | ) |
28 | 23 |
|
@@ -275,84 +270,162 @@ func Test_BundleValidatorCallsAllValidationFnsInOrder(t *testing.T) { |
275 | 270 | require.NoError(t, val.Validate(nil)) |
276 | 271 | require.Equal(t, "hi", actual) |
277 | 272 | } |
| 273 | +func Test_Render_WithNilBundleValidator(t *testing.T) { |
| 274 | + defer func() { |
| 275 | + if r := recover(); r == nil { |
| 276 | + t.Error("expected panic when BundleValidator is nil") |
| 277 | + } |
| 278 | + }() |
278 | 279 |
|
279 | | -// This test ensures consistent rendering behavior for registry+v1 bundles: |
280 | | -// - Uses the bundles defined in the testdata directory |
281 | | -// - Renders manifests using the registryv1 renderer |
282 | | -// - Sorts rendered objects by Kind, Namespace, and Name to ensure deterministic output |
283 | | -// - Writes the results to a temporary directory for inspection |
284 | | -// - Ensures the rendering completes without error |
285 | | -// - Automatically cleans up the generated files after the test completes |
286 | | -// |
287 | | -// This was introduced to prevent flaky diffs caused by non-deterministic manifest ordering. |
288 | | -// Related issue: https://github.com/operator-framework/operator-controller/pull/1895 |
289 | | -func Test_RenderRegistryV1Bundle_GenerateAndAutoCleanup(t *testing.T) { |
290 | | - bundleRoot := "testdata/bundles" |
| 280 | + renderer := render.BundleRenderer{} |
| 281 | + _, _ = renderer.Render(bundle.RegistryV1{}, "ns") |
| 282 | +} |
| 283 | + |
| 284 | +func Test_Render_WithNoResourceGenerators(t *testing.T) { |
| 285 | + renderer := render.BundleRenderer{ |
| 286 | + BundleValidator: render.BundleValidator{ |
| 287 | + func(_ *bundle.RegistryV1) []error { return nil }, |
| 288 | + }, |
| 289 | + } |
| 290 | + |
| 291 | + _, err := renderer.Render(bundle.RegistryV1{ |
| 292 | + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), |
| 293 | + }, "test", render.WithTargetNamespaces("test"), render.WithUniqueNameGenerator(render.DefaultUniqueNameGenerator)) |
| 294 | + |
| 295 | + require.NoError(t, err) |
| 296 | +} |
| 297 | + |
| 298 | +func Test_Render_ReturnsExactSameObjectReferences(t *testing.T) { |
| 299 | + obj1 := fakeUnstructured("Service", "ns", "svc") |
| 300 | + obj2 := fakeUnstructured("ConfigMap", "ns", "cfg") |
| 301 | + |
| 302 | + mockGen := render.ResourceGenerator(func(_ *bundle.RegistryV1, _ render.Options) ([]client.Object, error) { |
| 303 | + return []client.Object{obj1, obj2}, nil |
| 304 | + }) |
| 305 | + |
| 306 | + renderer := render.BundleRenderer{ |
| 307 | + BundleValidator: render.BundleValidator{func(_ *bundle.RegistryV1) []error { return nil }}, |
| 308 | + ResourceGenerators: []render.ResourceGenerator{mockGen}, |
| 309 | + } |
| 310 | + |
| 311 | + result, err := renderer.Render(bundle.RegistryV1{ |
| 312 | + CSV: MakeCSV(WithInstallModeSupportFor(v1alpha1.InstallModeTypeAllNamespaces)), |
| 313 | + }, "ns", render.WithTargetNamespaces("ns"), render.WithUniqueNameGenerator(render.DefaultUniqueNameGenerator)) |
291 | 314 |
|
| 315 | + require.NoError(t, err) |
| 316 | + require.Same(t, obj1, result[0]) |
| 317 | + require.Same(t, obj2, result[1]) |
| 318 | +} |
| 319 | + |
| 320 | +// Test_Render_ValidatesOutputForAllInstallModes ensures that the BundleRenderer |
| 321 | +// correctly generates and returns the exact list of client.Objects produced by the |
| 322 | +// ResourceGenerators, across all supported install modes (AllNamespaces, SingleNamespace, OwnNamespace). |
| 323 | +func Test_Render_ValidatesOutputForAllInstallModes(t *testing.T) { |
292 | 324 | testCases := []struct { |
293 | 325 | name string |
294 | 326 | installNamespace string |
295 | 327 | watchNamespace string |
296 | | - bundle string |
297 | | - testCaseName string |
| 328 | + installModes []v1alpha1.InstallMode |
| 329 | + expectedNS string |
298 | 330 | }{ |
299 | 331 | { |
300 | 332 | name: "AllNamespaces", |
301 | | - installNamespace: "argocd-system", |
| 333 | + installNamespace: "mock-system", |
302 | 334 | watchNamespace: "", |
303 | | - bundle: "argocd-operator.v0.6.0", |
304 | | - testCaseName: "all-namespaces", |
| 335 | + installModes: []v1alpha1.InstallMode{ |
| 336 | + {Type: v1alpha1.InstallModeTypeAllNamespaces, Supported: true}, |
| 337 | + }, |
| 338 | + expectedNS: "mock-system", |
305 | 339 | }, |
306 | 340 | { |
307 | | - name: "SingleNamespaces", |
308 | | - installNamespace: "argocd-system", |
309 | | - watchNamespace: "argocd-watch", |
310 | | - bundle: "argocd-operator.v0.6.0", |
311 | | - testCaseName: "single-namespace", |
| 341 | + name: "SingleNamespace", |
| 342 | + installNamespace: "mock-system", |
| 343 | + watchNamespace: "mock-watch", |
| 344 | + installModes: []v1alpha1.InstallMode{ |
| 345 | + {Type: v1alpha1.InstallModeTypeSingleNamespace, Supported: true}, |
| 346 | + }, |
| 347 | + expectedNS: "mock-watch", |
312 | 348 | }, |
313 | 349 | { |
314 | | - name: "OwnNamespaces", |
315 | | - installNamespace: "argocd-system", |
316 | | - watchNamespace: "argocd-system", |
317 | | - bundle: "argocd-operator.v0.6.0", |
318 | | - testCaseName: "own-namespace", |
| 350 | + name: "OwnNamespace", |
| 351 | + installNamespace: "mock-system", |
| 352 | + watchNamespace: "mock-system", |
| 353 | + installModes: []v1alpha1.InstallMode{ |
| 354 | + {Type: v1alpha1.InstallModeTypeOwnNamespace, Supported: true}, |
| 355 | + }, |
| 356 | + expectedNS: "mock-system", |
319 | 357 | }, |
320 | 358 | } |
321 | 359 |
|
322 | 360 | for _, tc := range testCases { |
323 | 361 | t.Run(tc.name, func(t *testing.T) { |
324 | | - bundlePath := filepath.Join(bundleRoot, tc.bundle) |
325 | | - outputDir := filepath.Join(t.TempDir(), tc.bundle, tc.testCaseName) |
| 362 | + // Given the mocks |
| 363 | + expectedObjects := []client.Object{ |
| 364 | + fakeUnstructured("ClusterRole", "", "mock-operator.v0-role"), |
| 365 | + fakeUnstructured("ClusterRoleBinding", "", "mock-operator.v0-binding"), |
| 366 | + fakeUnstructured("ConfigMap", tc.expectedNS, "mock-operator-manager-config"), |
| 367 | + fakeUnstructured("CustomResourceDefinition", "", "mocks.argoproj.io"), |
| 368 | + fakeUnstructured("Deployment", tc.expectedNS, "mock-operator-controller-manager"), |
| 369 | + fakeUnstructured("Service", tc.expectedNS, "mock-operator-controller-manager-metrics-service"), |
| 370 | + fakeUnstructured("ServiceAccount", tc.expectedNS, "mock-operator-controller-manager"), |
| 371 | + } |
326 | 372 |
|
327 | | - fs := os.DirFS(bundlePath) |
328 | | - bundle, err := source.FromFS(fs).GetBundle() |
329 | | - require.NoError(t, err) |
| 373 | + mockGen := render.ResourceGenerator(func(_ *bundle.RegistryV1, _ render.Options) ([]client.Object, error) { |
| 374 | + return expectedObjects, nil |
| 375 | + }) |
330 | 376 |
|
331 | | - rendered, err := registryv1.Renderer.Render(bundle, tc.installNamespace, |
332 | | - render.WithTargetNamespaces(tc.watchNamespace)) |
333 | | - require.NoError(t, err) |
| 377 | + mockBundle := bundle.RegistryV1{ |
| 378 | + CSV: v1alpha1.ClusterServiceVersion{ |
| 379 | + Spec: v1alpha1.ClusterServiceVersionSpec{ |
| 380 | + InstallModes: tc.installModes, |
| 381 | + }, |
| 382 | + }, |
| 383 | + } |
334 | 384 |
|
335 | | - sorted := slices.SortedFunc(slices.Values(rendered), orderByKindNamespaceName) |
| 385 | + renderer := render.BundleRenderer{ |
| 386 | + BundleValidator: render.BundleValidator{ |
| 387 | + func(_ *bundle.RegistryV1) []error { return nil }, |
| 388 | + }, |
| 389 | + ResourceGenerators: []render.ResourceGenerator{mockGen}, |
| 390 | + } |
336 | 391 |
|
337 | | - require.NoError(t, os.MkdirAll(outputDir, os.ModePerm)) |
| 392 | + opts := []render.Option{ |
| 393 | + render.WithTargetNamespaces(tc.watchNamespace), |
| 394 | + render.WithUniqueNameGenerator(render.DefaultUniqueNameGenerator), |
| 395 | + } |
338 | 396 |
|
339 | | - for idx, obj := range sorted { |
340 | | - kind := obj.GetObjectKind().GroupVersionKind().Kind |
341 | | - filename := fmt.Sprintf("%02d_%s_%s.yaml", idx, strings.ToLower(kind), obj.GetName()) |
342 | | - filePath := filepath.Join(outputDir, filename) |
| 397 | + // When we call: |
| 398 | + objs, err := renderer.Render(mockBundle, tc.installNamespace, opts...) |
343 | 399 |
|
344 | | - data, err := yaml.Marshal(obj) |
345 | | - require.NoError(t, err) |
346 | | - require.NoError(t, os.WriteFile(filePath, data, 0600)) |
| 400 | + // We expected have no error and ensure that the objects |
| 401 | + // returned match with the resource generator's output. |
| 402 | + require.NoError(t, err) |
| 403 | + require.Len(t, objs, len(expectedObjects)) |
| 404 | + |
| 405 | + for i, obj := range objs { |
| 406 | + exp := expectedObjects[i] |
| 407 | + assert.Equal(t, exp.GetObjectKind().GroupVersionKind().Kind, obj.GetObjectKind().GroupVersionKind().Kind) |
| 408 | + assert.Equal(t, exp.GetName(), obj.GetName(), "unexpected name at index %d", i) |
| 409 | + assert.Equal(t, exp.GetNamespace(), obj.GetNamespace(), "unexpected namespace at index %d", i) |
347 | 410 | } |
348 | 411 | }) |
349 | 412 | } |
350 | 413 | } |
351 | 414 |
|
352 | | -func orderByKindNamespaceName(a client.Object, b client.Object) int { |
353 | | - return cmp.Or( |
354 | | - cmp.Compare(a.GetObjectKind().GroupVersionKind().Kind, b.GetObjectKind().GroupVersionKind().Kind), |
355 | | - cmp.Compare(a.GetNamespace(), b.GetNamespace()), |
356 | | - cmp.Compare(a.GetName(), b.GetName()), |
357 | | - ) |
| 415 | +func fakeUnstructured(kind, namespace, name string) client.Object { |
| 416 | + obj := &unstructured.Unstructured{} |
| 417 | + group := "" |
| 418 | + version := "v1" |
| 419 | + if kind == "CustomResourceDefinition" { |
| 420 | + group = "apiextensions.k8s.io" |
| 421 | + version = "v1" |
| 422 | + } |
| 423 | + obj.SetGroupVersionKind(schema.GroupVersionKind{ |
| 424 | + Group: group, |
| 425 | + Version: version, |
| 426 | + Kind: kind, |
| 427 | + }) |
| 428 | + obj.SetNamespace(namespace) |
| 429 | + obj.SetName(name) |
| 430 | + return obj |
358 | 431 | } |
0 commit comments