Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions pkg/lint/checks/workloads/kserve/impacted.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package kserve
import (
"context"
"fmt"
"io"
"sort"
"strconv"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -11,6 +13,7 @@ import (
"github.com/opendatahub-io/odh-cli/pkg/constants"
"github.com/opendatahub-io/odh-cli/pkg/lint/check"
"github.com/opendatahub-io/odh-cli/pkg/lint/check/result"
"github.com/opendatahub-io/odh-cli/pkg/printer/table"
"github.com/opendatahub-io/odh-cli/pkg/resources"
"github.com/opendatahub-io/odh-cli/pkg/util/client"
"github.com/opendatahub-io/odh-cli/pkg/util/components"
Expand Down Expand Up @@ -48,6 +51,10 @@ const (
// ImpactedWorkloadsCheck lists InferenceServices and ServingRuntimes using deprecated deployment modes.
type ImpactedWorkloadsCheck struct {
check.BaseCheck

// deploymentModeFilter filters InferenceServices by deployment mode in verbose output.
// Valid values: "all" (default), "serverless", "modelmesh".
deploymentModeFilter string
}

func NewImpactedWorkloadsCheck() *ImpactedWorkloadsCheck {
Expand All @@ -61,9 +68,16 @@ func NewImpactedWorkloadsCheck() *ImpactedWorkloadsCheck {
CheckDescription: "Lists InferenceServices and ServingRuntimes using deprecated deployment modes (ModelMesh, Serverless), removed ServingRuntimes, or ServingRuntimes referencing deprecated AcceleratorProfiles that will be impacted in RHOAI 3.x",
CheckRemediation: "Migrate InferenceServices from Serverless/ModelMesh to RawDeployment mode, update ServingRuntimes to supported versions, and review AcceleratorProfile references before upgrading",
},
deploymentModeFilter: "all", // Default to showing all deployment modes
}
}

// SetDeploymentModeFilter sets the filter for InferenceService display by deployment mode.
// Valid values: "all", "serverless", "modelmesh".
func (c *ImpactedWorkloadsCheck) SetDeploymentModeFilter(filter string) {
c.deploymentModeFilter = filter
}

// CanApply returns whether this check should run for the given target.
// Only applies when upgrading FROM 2.x TO 3.x and KServe or ModelMesh is Managed.
func (c *ImpactedWorkloadsCheck) CanApply(ctx context.Context, target check.Target) (bool, error) {
Expand Down Expand Up @@ -164,3 +178,87 @@ func (c *ImpactedWorkloadsCheck) Validate(

return dr, nil
}

// inferenceServiceRow represents a row in the InferenceService detail table.
type inferenceServiceRow struct {
Name string `mapstructure:"NAME"`
Namespace string `mapstructure:"NAMESPACE"`
DeploymentMode string `mapstructure:"DEPLOYMENT MODE"`
}

// FormatVerboseOutput provides custom formatting for InferenceServices in verbose mode.
// Displays a detailed table showing Name, Namespace, and DeploymentMode for each InferenceService.
// Filters InferenceServices based on the deploymentModeFilter setting.
func (c *ImpactedWorkloadsCheck) FormatVerboseOutput(out io.Writer, dr *result.DiagnosticResult) {
// Collect InferenceServices from impacted objects
var isvcs []inferenceServiceRow

for _, obj := range dr.ImpactedObjects {
if obj.Kind != "InferenceService" {
continue
}

deploymentMode := obj.Annotations[annotationDeploymentMode]
if deploymentMode == "" {
// Check for runtime annotation (for removed runtime ISVCs)
if runtime := obj.Annotations["serving.kserve.io/runtime"]; runtime != "" {
deploymentMode = "RawDeployment"
} else {
deploymentMode = "Unknown"
}
}

// Apply deployment mode filter
if c.deploymentModeFilter != "all" {
filterMode := ""
switch c.deploymentModeFilter {
case "serverless":
filterMode = deploymentModeServerless
case "modelmesh":
filterMode = deploymentModeModelMesh
}

if deploymentMode != filterMode {
continue
}
}

isvcs = append(isvcs, inferenceServiceRow{
Name: obj.Name,
Namespace: obj.Namespace,
DeploymentMode: deploymentMode,
})
}

if len(isvcs) == 0 {
return
}

// Sort by namespace, then by name
sort.Slice(isvcs, func(i, j int) bool {
if isvcs[i].Namespace != isvcs[j].Namespace {
return isvcs[i].Namespace < isvcs[j].Namespace
}

return isvcs[i].Name < isvcs[j].Name
})

// Render table with InferenceService details
renderer := table.NewRenderer[inferenceServiceRow](
table.WithWriter[inferenceServiceRow](out),
table.WithHeaders[inferenceServiceRow]("NAME", "NAMESPACE", "DEPLOYMENT MODE"),
table.WithTableOptions[inferenceServiceRow](table.DefaultTableOptions...),
)

for _, isvc := range isvcs {
if err := renderer.Append(isvc); err != nil {
_, _ = fmt.Fprintf(out, " Error rendering InferenceService: %v\n", err)

return
}
}

if err := renderer.Render(); err != nil {
_, _ = fmt.Fprintf(out, " Error rendering table: %v\n", err)
}
}
73 changes: 73 additions & 0 deletions pkg/lint/checks/workloads/kserve/impacted_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kserve_test

import (
"strings"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -1253,3 +1254,75 @@ func TestImpactedWorkloadsCheck_AcceleratorSR_MixedAnnotations(t *testing.T) {
// 2 SRs + 1 ISVC = 3 impacted objects
g.Expect(result.Annotations).To(HaveKeyWithValue(check.AnnotationImpactedWorkloadCount, "3"))
}

func TestImpactedWorkloadsCheck_FormatVerboseOutput(t *testing.T) {
g := NewWithT(t)

// Create a diagnostic result with multiple InferenceServices
dr := resultpkg.New("workload", "kserve", "impacted-workloads", "Test description")
dr.ImpactedObjects = []metav1.PartialObjectMetadata{
{
TypeMeta: resources.InferenceService.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-b",
Name: "isvc-modelmesh",
Annotations: map[string]string{
annotationDeploymentMode: "ModelMesh",
},
},
},
{
TypeMeta: resources.InferenceService.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-a",
Name: "isvc-serverless",
Annotations: map[string]string{
annotationDeploymentMode: "Serverless",
},
},
},
{
TypeMeta: resources.ServingRuntime.TypeMeta(),
ObjectMeta: metav1.ObjectMeta{
Namespace: "ns-a",
Name: "some-runtime",
},
},
}

chk := kserve.NewImpactedWorkloadsCheck()
var buf strings.Builder
chk.FormatVerboseOutput(&buf, dr)

output := buf.String()

// Verify table headers are present
g.Expect(output).To(ContainSubstring("NAME"))
g.Expect(output).To(ContainSubstring("NAMESPACE"))
g.Expect(output).To(ContainSubstring("DEPLOYMENT MODE"))

// Verify InferenceServices are present (sorted by namespace then name)
g.Expect(output).To(ContainSubstring("isvc-serverless"))
g.Expect(output).To(ContainSubstring("ns-a"))
g.Expect(output).To(ContainSubstring("Serverless"))

g.Expect(output).To(ContainSubstring("isvc-modelmesh"))
g.Expect(output).To(ContainSubstring("ns-b"))
g.Expect(output).To(ContainSubstring("ModelMesh"))

// Verify ServingRuntime is NOT included (only InferenceServices)
g.Expect(output).ToNot(ContainSubstring("some-runtime"))
}

func TestImpactedWorkloadsCheck_FormatVerboseOutput_EmptyResult(t *testing.T) {
g := NewWithT(t)

dr := resultpkg.New("workload", "kserve", "impacted-workloads", "Test description")

chk := kserve.NewImpactedWorkloadsCheck()
var buf strings.Builder
chk.FormatVerboseOutput(&buf, dr)

// Empty result should produce no output
g.Expect(buf.String()).To(BeEmpty())
}
30 changes: 28 additions & 2 deletions pkg/lint/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package lint
import (
"context"
"fmt"
"slices"

"github.com/blang/semver/v4"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -47,6 +48,10 @@ type Command struct {
// If set, runs in upgrade mode (assesses upgrade readiness to target version).
TargetVersion string

// ISVCDeploymentMode filters InferenceService display by deployment mode.
// Valid values: "all" (default), "serverless", "modelmesh".
ISVCDeploymentMode string

// parsedTargetVersion is the parsed semver version (upgrade mode only)
parsedTargetVersion *semver.Version

Expand Down Expand Up @@ -115,8 +120,9 @@ func NewCommand(
registry.MustRegister(trainingoperatorworkloads.NewImpactedWorkloadsCheck())

c := &Command{
SharedOptions: shared,
registry: registry,
SharedOptions: shared,
registry: registry,
ISVCDeploymentMode: "all",
}

// Apply functional options
Expand All @@ -135,6 +141,7 @@ func (c *Command) AddFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&c.Verbose, "verbose", "v", false, flagDescVerbose)
fs.BoolVar(&c.Debug, "debug", false, flagDescDebug)
fs.DurationVar(&c.Timeout, "timeout", c.Timeout, flagDescTimeout)
fs.StringVar(&c.ISVCDeploymentMode, "isvc-deployment-mode", "all", flagDescISVCDeploymentMode)

// Throttling settings
fs.Float32Var(&c.QPS, "qps", c.QPS, flagDescQPS)
Expand Down Expand Up @@ -174,6 +181,12 @@ func (c *Command) Validate() error {
return fmt.Errorf("validating shared options: %w", err)
}

// Validate ISVC deployment mode filter
validModes := []string{"all", "serverless", "modelmesh"}
if !slices.Contains(validModes, c.ISVCDeploymentMode) {
return fmt.Errorf("invalid isvc-deployment-mode: %s (must be one of: all, serverless, modelmesh)", c.ISVCDeploymentMode)
}

return nil
}

Expand Down Expand Up @@ -222,6 +235,16 @@ func (c *Command) Run(ctx context.Context) error {
return c.runUpgradeMode(ctx, currentVersion)
}

// configureCheckSettings applies command-level settings to specific checks.
func (c *Command) configureCheckSettings() {
// Apply ISVC deployment mode filter to the KServe impacted workloads check
for _, chk := range c.registry.ListAll() {
if isvcCheck, ok := chk.(*kserveworkloads.ImpactedWorkloadsCheck); ok {
isvcCheck.SetDeploymentModeFilter(c.ISVCDeploymentMode)
}
}
}

// runLintMode validates current cluster state.
//
//nolint:unparam // keep explicit error return value
Expand All @@ -243,6 +266,9 @@ func (c *Command) runLintMode(_ context.Context, currentVersion *semver.Version)
func (c *Command) runUpgradeMode(ctx context.Context, currentVersion *semver.Version) error {
c.IO.Errorf("Assessing upgrade readiness: %s → %s\n", currentVersion.String(), c.TargetVersion)

// Configure check-specific settings
c.configureCheckSettings()

// Execute checks using target version for applicability filtering
c.IO.Errorf("Running upgrade compatibility checks...")
executor := check.NewExecutor(c.registry, c.IO)
Expand Down
15 changes: 8 additions & 7 deletions pkg/lint/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package lint

// Flag descriptions for the lint command.
const (
flagDescTargetVersion = "target version for upgrade readiness checks (e.g., 2.25.0, 3.0.0)"
flagDescOutput = "output format (table|json|yaml)"
flagDescVerbose = "show impacted objects and summary information"
flagDescDebug = "show detailed diagnostic logs for troubleshooting"
flagDescTimeout = "operation timeout (e.g., 10m, 30m)"
flagDescQPS = "Kubernetes API QPS limit (queries per second)"
flagDescBurst = "Kubernetes API burst capacity"
flagDescTargetVersion = "target version for upgrade readiness checks (e.g., 2.25.0, 3.0.0)"
flagDescOutput = "output format (table|json|yaml)"
flagDescVerbose = "show impacted objects and summary information"
flagDescDebug = "show detailed diagnostic logs for troubleshooting"
flagDescTimeout = "operation timeout (e.g., 10m, 30m)"
flagDescQPS = "Kubernetes API QPS limit (queries per second)"
flagDescBurst = "Kubernetes API burst capacity"
flagDescISVCDeploymentMode = "filter InferenceService display by deployment mode (all|serverless|modelmesh)"
)

const flagDescChecks = `check selector patterns (glob patterns or categories):
Expand Down