diff --git a/cmd/gpuop-cfg/validate/clusterpolicy/clusterpolicy_test.go b/cmd/gpuop-cfg/validate/clusterpolicy/clusterpolicy_test.go new file mode 100644 index 0000000000..000060e1ee --- /dev/null +++ b/cmd/gpuop-cfg/validate/clusterpolicy/clusterpolicy_test.go @@ -0,0 +1,46 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 clusterpolicy + +import ( + "slices" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func TestNewCommand(t *testing.T) { + cmd := NewCommand(logrus.New()) + + require.NotNil(t, cmd) + assert.Equal(t, "clusterpolicy", cmd.Name) + assert.NotEmpty(t, cmd.Usage) + + idx := slices.IndexFunc(cmd.Flags, func(f cli.Flag) bool { + return slices.Contains(f.Names(), "input") + }) + require.GreaterOrEqual(t, idx, 0) + + inputFlag, ok := cmd.Flags[idx].(*cli.StringFlag) + require.True(t, ok) + assert.Contains(t, inputFlag.Names(), "input") + assert.NotEmpty(t, inputFlag.Usage) + assert.Equal(t, "-", inputFlag.Value) +} diff --git a/cmd/gpuop-cfg/validate/clusterpolicy/images_test.go b/cmd/gpuop-cfg/validate/clusterpolicy/images_test.go new file mode 100644 index 0000000000..86523ffadc --- /dev/null +++ b/cmd/gpuop-cfg/validate/clusterpolicy/images_test.go @@ -0,0 +1,67 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 clusterpolicy + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1" +) + +func TestValidateImage_InvalidReference(t *testing.T) { + testCases := []struct { + description string + path string + }{ + { + description: "empty reference", + path: "", + }, + { + description: "malformed reference", + path: "@@bad::ref", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + err := validateImage(context.Background(), tc.path) + require.ErrorContains(t, err, "failed to construct an image reference") + }) + } +} + +func TestValidateImages_EmptyDriverSpecImagePathError(t *testing.T) { + t.Setenv("DRIVER_IMAGE", "") + + spec := &v1.ClusterPolicySpec{} + + err := validateImages(context.Background(), spec) + require.ErrorContains(t, err, "failed to construct the image path") +} + +func TestValidateImages_InvalidDriverImageRefError(t *testing.T) { + spec := &v1.ClusterPolicySpec{} + spec.Driver.Image = "@@bad::ref" + + err := validateImages(context.Background(), spec) + require.ErrorContains(t, err, "failed to validate image") + require.ErrorContains(t, err, "failed to construct an image reference") +} diff --git a/cmd/gpuop-cfg/validate/clusterpolicy/options_test.go b/cmd/gpuop-cfg/validate/clusterpolicy/options_test.go new file mode 100644 index 0000000000..060517143b --- /dev/null +++ b/cmd/gpuop-cfg/validate/clusterpolicy/options_test.go @@ -0,0 +1,90 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 clusterpolicy + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOptionsLoad(t *testing.T) { + const validManifest = `apiVersion: nvidia.com/v1 +kind: ClusterPolicy +metadata: + name: cluster-policy +` + + t.Run("valid manifest", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "clusterpolicy.yaml") + require.NoError(t, os.WriteFile(path, []byte(validManifest), 0o600)) + + spec, err := (options{input: path}).load() + + require.NoError(t, err) + assert.Equal(t, "cluster-policy", spec.Name) + }) + + t.Run("missing file", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "does-not-exist.yaml") + + _, err := (options{input: path}).load() + + require.ErrorContains(t, err, "failed to read file") + }) + + t.Run("malformed yaml", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "malformed.yaml") + require.NoError(t, os.WriteFile(path, []byte("\tnot: : valid: yaml"), 0o600)) + + _, err := (options{input: path}).load() + + require.ErrorContains(t, err, "failed to unmarshal spec") + }) + + t.Run("empty file", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "empty.yaml") + require.NoError(t, os.WriteFile(path, []byte(""), 0o600)) + + spec, err := (options{input: path}).load() + + require.NoError(t, err) + assert.Empty(t, spec.Name) + }) +} + +func TestOptionsGetContentsStdin(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + + orig := os.Stdin + os.Stdin = r + t.Cleanup(func() { os.Stdin = orig }) + + go func() { + _, _ = w.Write([]byte("hello")) + _ = w.Close() + }() + + got, err := (options{input: "-"}).getContents() + + require.NoError(t, err) + assert.Equal(t, "hello", string(got)) +} diff --git a/cmd/gpuop-cfg/validate/csv/alm-examples_test.go b/cmd/gpuop-cfg/validate/csv/alm-examples_test.go new file mode 100644 index 0000000000..27679092b5 --- /dev/null +++ b/cmd/gpuop-cfg/validate/csv/alm-examples_test.go @@ -0,0 +1,140 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 csv + +import ( + "testing" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateALMExample(t *testing.T) { + testCases := []struct { + description string + annotations map[string]string + expectError bool + errContains string + }{ + { + description: "first item with Kind ClusterPolicy returns nil", + annotations: map[string]string{ + "alm-examples": `[{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1","spec":{}}]`, + }, + expectError: false, + }, + { + description: "Kind ClusterPolicy with wrong apiVersion returns nil (apiVersion is not checked)", + annotations: map[string]string{ + "alm-examples": `[{"kind":"ClusterPolicy","apiVersion":"example.com/v99","spec":{}}]`, + }, + expectError: false, + }, + { + description: "multi-entry with ClusterPolicy first ignores later entries and returns nil", + annotations: map[string]string{ + "alm-examples": `[{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1","spec":{}},{"kind":"SomethingElse","apiVersion":"nvidia.com/v1"}]`, + }, + expectError: false, + }, + { + description: "malformed JSON returns an unmarshal error", + annotations: map[string]string{ + "alm-examples": `{not valid json`, + }, + expectError: true, + errContains: "invalid character", + }, + { + description: "missing alm-examples annotation returns an unmarshal error on empty string", + annotations: map[string]string{}, + expectError: true, + errContains: "unexpected end of JSON input", + }, + { + description: "nil annotations returns an unmarshal error on empty string", + annotations: nil, + expectError: true, + errContains: "unexpected end of JSON input", + }, + { + description: "empty alm-examples annotation returns an unmarshal error on empty string", + annotations: map[string]string{ + "alm-examples": "", + }, + expectError: true, + errContains: "unexpected end of JSON input", + }, + { + description: "empty list returns 'no example clusterpolicy found'", + annotations: map[string]string{ + "alm-examples": `[]`, + }, + expectError: true, + errContains: "no example clusterpolicy found", + }, + { + description: "JSON null returns 'no example clusterpolicy found'", + annotations: map[string]string{ + "alm-examples": "null", + }, + expectError: true, + errContains: "no example clusterpolicy found", + }, + { + description: "JSON object instead of array returns an unmarshal error", + annotations: map[string]string{ + "alm-examples": `{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1"}`, + }, + expectError: true, + errContains: "cannot unmarshal object", + }, + { + description: "first item with Kind != ClusterPolicy returns 'invalid example clusterpolicy'", + annotations: map[string]string{ + "alm-examples": `[{"kind":"NotAClusterPolicy","apiVersion":"nvidia.com/v1"}]`, + }, + expectError: true, + errContains: "invalid example clusterpolicy", + }, + { + description: "multi-entry with ClusterPolicy not first returns 'invalid example clusterpolicy'", + annotations: map[string]string{ + "alm-examples": `[{"kind":"SomethingElse","apiVersion":"nvidia.com/v1"},{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1","spec":{}}]`, + }, + expectError: true, + errContains: "invalid example clusterpolicy", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + csv := &v1alpha1.ClusterServiceVersion{} + csv.Annotations = tc.annotations + + err := validateALMExample(csv) + + if !tc.expectError { + require.NoError(t, err) + return + } + + assert.ErrorContains(t, err, tc.errContains) + }) + } +} diff --git a/cmd/gpuop-cfg/validate/csv/csv_test.go b/cmd/gpuop-cfg/validate/csv/csv_test.go new file mode 100644 index 0000000000..567bc59a2b --- /dev/null +++ b/cmd/gpuop-cfg/validate/csv/csv_test.go @@ -0,0 +1,46 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 csv + +import ( + "slices" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func TestNewCommand(t *testing.T) { + cmd := NewCommand(logrus.New()) + + require.NotNil(t, cmd) + assert.Equal(t, "csv", cmd.Name) + assert.NotEmpty(t, cmd.Usage) + + idx := slices.IndexFunc(cmd.Flags, func(f cli.Flag) bool { + return slices.Contains(f.Names(), "input") + }) + require.GreaterOrEqual(t, idx, 0) + + inputFlag, ok := cmd.Flags[idx].(*cli.StringFlag) + require.True(t, ok) + assert.Contains(t, inputFlag.Names(), "input") + assert.NotEmpty(t, inputFlag.Usage) + assert.Equal(t, "-", inputFlag.Value) +} diff --git a/cmd/gpuop-cfg/validate/csv/images_test.go b/cmd/gpuop-cfg/validate/csv/images_test.go new file mode 100644 index 0000000000..2de6a78204 --- /dev/null +++ b/cmd/gpuop-cfg/validate/csv/images_test.go @@ -0,0 +1,59 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 csv + +import ( + "context" + "testing" + + "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/stretchr/testify/require" +) + +func TestValidateImage_InvalidReference(t *testing.T) { + testCases := []struct { + description string + path string + }{ + { + description: "empty reference", + path: "", + }, + { + description: "malformed reference", + path: "@@bad::ref", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + err := validateImage(context.Background(), tc.path) + require.ErrorContains(t, err, "failed to construct an image reference") + }) + } +} + +func TestValidateImages_InvalidRelatedImage(t *testing.T) { + csv := &v1alpha1.ClusterServiceVersion{} + csv.Spec.RelatedImages = []v1alpha1.RelatedImage{ + {Name: "bad-image", Image: "@@bad::ref"}, + } + + err := validateImages(context.Background(), csv) + require.ErrorContains(t, err, "failed to validate image bad-image") + require.ErrorContains(t, err, "failed to construct an image reference") +} diff --git a/cmd/gpuop-cfg/validate/csv/options_test.go b/cmd/gpuop-cfg/validate/csv/options_test.go new file mode 100644 index 0000000000..3d225a75f9 --- /dev/null +++ b/cmd/gpuop-cfg/validate/csv/options_test.go @@ -0,0 +1,90 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 csv + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOptionsLoad(t *testing.T) { + const validManifest = `apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: gpu-operator +` + + t.Run("valid manifest", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "csv.yaml") + require.NoError(t, os.WriteFile(path, []byte(validManifest), 0o600)) + + spec, err := (options{input: path}).load() + + require.NoError(t, err) + assert.Equal(t, "gpu-operator", spec.Name) + }) + + t.Run("missing file", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "does-not-exist.yaml") + + _, err := (options{input: path}).load() + + require.ErrorContains(t, err, "failed to read file") + }) + + t.Run("malformed yaml", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "malformed.yaml") + require.NoError(t, os.WriteFile(path, []byte("\tnot: : valid: yaml"), 0o600)) + + _, err := (options{input: path}).load() + + require.ErrorContains(t, err, "failed to unmarshal spec") + }) + + t.Run("empty file", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "empty.yaml") + require.NoError(t, os.WriteFile(path, []byte(""), 0o600)) + + spec, err := (options{input: path}).load() + + require.NoError(t, err) + assert.Empty(t, spec.Name) + }) +} + +func TestOptionsGetContentsStdin(t *testing.T) { + r, w, err := os.Pipe() + require.NoError(t, err) + + orig := os.Stdin + os.Stdin = r + t.Cleanup(func() { os.Stdin = orig }) + + go func() { + _, _ = w.Write([]byte("hello")) + _ = w.Close() + }() + + got, err := (options{input: "-"}).getContents() + + require.NoError(t, err) + assert.Equal(t, "hello", string(got)) +} diff --git a/cmd/gpuop-cfg/validate/validate_test.go b/cmd/gpuop-cfg/validate/validate_test.go new file mode 100644 index 0000000000..42b85c0ac8 --- /dev/null +++ b/cmd/gpuop-cfg/validate/validate_test.go @@ -0,0 +1,40 @@ +/** +# Copyright (c) NVIDIA CORPORATION. All rights reserved. +# +# 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 validate + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewCommand(t *testing.T) { + cmd := NewCommand(logrus.New()) + + require.NotNil(t, cmd) + assert.Equal(t, "validate", cmd.Name) + assert.NotEmpty(t, cmd.Usage) + + names := []string{} + for _, sub := range cmd.Commands { + names = append(names, sub.Name) + } + assert.Contains(t, names, "csv") + assert.Contains(t, names, "clusterpolicy") +}