From 360527f353f1b8aa4f3d018ac4a87c9b7bdee082 Mon Sep 17 00:00:00 2001 From: Amos Mastbaum Date: Fri, 3 Jul 2026 07:12:29 +0300 Subject: [PATCH 1/2] feat(openshift): add standalone RHOAI deployment on any OCP cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the profile system (RHOAI, ServiceMesh, Serverless, etc.) from pkg/target/service/snc/profile/ into pkg/provider/openshift/profile/ so it can be used independently of SNC. The profile code had zero SNC dependencies — only the package path coupled it. Add new `mapt openshift rhoai create/destroy` command that deploys RHOAI on any existing OpenShift cluster given a kubeconfig path. This supports the AIPCC-19537 epic for RHAII integration testing on stable RHOAI. Usage: mapt openshift rhoai create --kubeconfig --project-name --backed-url mapt openshift rhoai destroy --project-name --backed-url Profiles default to "ai" which installs the full RHOAI stack (ServiceMesh v2, Serverless, RHOAI operator, DataScienceCluster with KServe). Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/mapt/cmd/aws/services/snc.go | 2 +- cmd/mapt/cmd/openshift/openshift.go | 33 ++++++ cmd/mapt/cmd/openshift/rhoai.go | 104 ++++++++++++++++++ cmd/mapt/cmd/root.go | 4 +- pkg/provider/aws/action/snc/snc.go | 2 +- pkg/provider/openshift/action/rhoai/rhoai.go | 95 ++++++++++++++++ pkg/provider/openshift/openshift.go | 36 ++++++ .../openshift}/profile/client.go | 0 .../snc => provider/openshift}/profile/nfd.go | 0 .../openshift}/profile/nvidia.go | 0 .../openshift}/profile/openshift_ai.go | 0 .../openshift}/profile/operator.go | 0 .../openshift}/profile/profile.go | 2 +- .../openshift}/profile/serverless.go | 0 .../openshift}/profile/servicemesh.go | 0 .../openshift}/profile/servicemesh_v2.go | 0 .../openshift}/profile/virtualization.go | 0 17 files changed, 274 insertions(+), 4 deletions(-) create mode 100644 cmd/mapt/cmd/openshift/openshift.go create mode 100644 cmd/mapt/cmd/openshift/rhoai.go create mode 100644 pkg/provider/openshift/action/rhoai/rhoai.go create mode 100644 pkg/provider/openshift/openshift.go rename pkg/{target/service/snc => provider/openshift}/profile/client.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/nfd.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/nvidia.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/openshift_ai.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/operator.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/profile.go (98%) rename pkg/{target/service/snc => provider/openshift}/profile/serverless.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/servicemesh.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/servicemesh_v2.go (100%) rename pkg/{target/service/snc => provider/openshift}/profile/virtualization.go (100%) diff --git a/cmd/mapt/cmd/aws/services/snc.go b/cmd/mapt/cmd/aws/services/snc.go index 0e1d07665..c912bf14e 100644 --- a/cmd/mapt/cmd/aws/services/snc.go +++ b/cmd/mapt/cmd/aws/services/snc.go @@ -6,7 +6,7 @@ import ( maptContext "github.com/redhat-developer/mapt/pkg/manager/context" openshiftsnc "github.com/redhat-developer/mapt/pkg/provider/aws/action/snc" sncApi "github.com/redhat-developer/mapt/pkg/target/service/snc" - "github.com/redhat-developer/mapt/pkg/target/service/snc/profile" + "github.com/redhat-developer/mapt/pkg/provider/openshift/profile" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" diff --git a/cmd/mapt/cmd/openshift/openshift.go b/cmd/mapt/cmd/openshift/openshift.go new file mode 100644 index 000000000..51f2c8d7c --- /dev/null +++ b/cmd/mapt/cmd/openshift/openshift.go @@ -0,0 +1,33 @@ +package openshift + +import ( + params "github.com/redhat-developer/mapt/cmd/mapt/cmd/params" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + cmd = "openshift" + cmdDesc = "operations on existing OpenShift clusters" +) + +func GetCmd() *cobra.Command { + c := &cobra.Command{ + Use: cmd, + Short: cmdDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + return nil + }, + } + + flagSet := pflag.NewFlagSet(cmd, pflag.ExitOnError) + params.AddCommonFlags(flagSet) + c.PersistentFlags().AddFlagSet(flagSet) + + c.AddCommand(getRHOAICmd()) + return c +} diff --git a/cmd/mapt/cmd/openshift/rhoai.go b/cmd/mapt/cmd/openshift/rhoai.go new file mode 100644 index 000000000..47c28bac3 --- /dev/null +++ b/cmd/mapt/cmd/openshift/rhoai.go @@ -0,0 +1,104 @@ +package openshift + +import ( + params "github.com/redhat-developer/mapt/cmd/mapt/cmd/params" + maptContext "github.com/redhat-developer/mapt/pkg/manager/context" + rhoaiAction "github.com/redhat-developer/mapt/pkg/provider/openshift/action/rhoai" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + cmdRHOAI = "rhoai" + cmdRHOAIDesc = "deploy Red Hat OpenShift AI on an existing OpenShift cluster" + + kubeconfig = "kubeconfig" + kubeconfigDesc = "path to the kubeconfig file for the target OpenShift cluster" + + rhoaiProfile = "profile" + rhoaiProfileDesc = "profiles to deploy (ai, virtualization, serverless-serving, serverless-eventing, serverless, servicemesh, nvidia)" +) + +func getRHOAICmd() *cobra.Command { + c := &cobra.Command{ + Use: cmdRHOAI, + Short: cmdRHOAIDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + return nil + }, + } + c.AddCommand(createRHOAI(), destroyRHOAI()) + return c +} + +func createRHOAI() *cobra.Command { + c := &cobra.Command{ + Use: params.CreateCmdName, + Short: params.CreateCmdName, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + if err := viper.BindPFlags(cmd.InheritedFlags()); err != nil { + return err + } + profiles := viper.GetStringSlice(rhoaiProfile) + if len(profiles) == 0 { + profiles = []string{"ai"} + } + return rhoaiAction.Create( + &maptContext.ContextArgs{ + Context: cmd.Context(), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + Tags: viper.GetStringMapString(params.Tags), + }, + &rhoaiAction.RHOAIArgs{ + KubeconfigPath: viper.GetString(kubeconfig), + Profiles: profiles, + }) + }, + } + flagSet := pflag.NewFlagSet(params.CreateCmdName, pflag.ExitOnError) + flagSet.StringP(kubeconfig, "", "", kubeconfigDesc) + flagSet.StringSliceP(rhoaiProfile, "", []string{}, rhoaiProfileDesc) + flagSet.StringToStringP(params.Tags, "", nil, params.TagsDesc) + c.PersistentFlags().AddFlagSet(flagSet) + _ = c.MarkFlagRequired(kubeconfig) + return c +} + +func destroyRHOAI() *cobra.Command { + c := &cobra.Command{ + Use: params.DestroyCmdName, + Short: params.DestroyCmdName, + RunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return err + } + if err := viper.BindPFlags(cmd.InheritedFlags()); err != nil { + return err + } + return rhoaiAction.Destroy(&maptContext.ContextArgs{ + Context: cmd.Context(), + ProjectName: viper.GetString(params.ProjectName), + BackedURL: viper.GetString(params.BackedURL), + Debug: viper.IsSet(params.Debug), + DebugLevel: viper.GetUint(params.DebugLevel), + ForceDestroy: viper.IsSet(params.ForceDestroy), + KeepState: viper.IsSet(params.KeepState), + }) + }, + } + flagSet := pflag.NewFlagSet(params.DestroyCmdName, pflag.ExitOnError) + flagSet.Bool(params.ForceDestroy, false, params.ForceDestroyDesc) + flagSet.Bool(params.KeepState, false, params.KeepStateDesc) + c.PersistentFlags().AddFlagSet(flagSet) + return c +} diff --git a/cmd/mapt/cmd/root.go b/cmd/mapt/cmd/root.go index 57094587f..293fb6827 100644 --- a/cmd/mapt/cmd/root.go +++ b/cmd/mapt/cmd/root.go @@ -6,6 +6,7 @@ import ( "github.com/redhat-developer/mapt/cmd/mapt/cmd/aws" "github.com/redhat-developer/mapt/cmd/mapt/cmd/azure" "github.com/redhat-developer/mapt/cmd/mapt/cmd/ibmcloud" + "github.com/redhat-developer/mapt/cmd/mapt/cmd/openshift" "github.com/redhat-developer/mapt/cmd/mapt/cmd/params" "github.com/redhat-developer/mapt/pkg/util/logging" "github.com/spf13/cobra" @@ -63,7 +64,8 @@ func init() { rootCmd.AddCommand( aws.GetCmd(), azure.GetCmd(), - ibmcloud.GetCmd()) + ibmcloud.GetCmd(), + openshift.GetCmd()) } func Execute() { diff --git a/pkg/provider/aws/action/snc/snc.go b/pkg/provider/aws/action/snc/snc.go index df643c51e..f7e7beec4 100644 --- a/pkg/provider/aws/action/snc/snc.go +++ b/pkg/provider/aws/action/snc/snc.go @@ -28,7 +28,7 @@ import ( "github.com/redhat-developer/mapt/pkg/provider/util/command" "github.com/redhat-developer/mapt/pkg/provider/util/security" apiSNC "github.com/redhat-developer/mapt/pkg/target/service/snc" - "github.com/redhat-developer/mapt/pkg/target/service/snc/profile" + "github.com/redhat-developer/mapt/pkg/provider/openshift/profile" "github.com/redhat-developer/mapt/pkg/util" "github.com/redhat-developer/mapt/pkg/util/logging" resourcesUtil "github.com/redhat-developer/mapt/pkg/util/resources" diff --git a/pkg/provider/openshift/action/rhoai/rhoai.go b/pkg/provider/openshift/action/rhoai/rhoai.go new file mode 100644 index 000000000..4191a0aa2 --- /dev/null +++ b/pkg/provider/openshift/action/rhoai/rhoai.go @@ -0,0 +1,95 @@ +package rhoai + +import ( + "fmt" + "os" + + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "github.com/redhat-developer/mapt/pkg/manager" + mc "github.com/redhat-developer/mapt/pkg/manager/context" + "github.com/redhat-developer/mapt/pkg/provider/openshift" + "github.com/redhat-developer/mapt/pkg/provider/openshift/profile" + "github.com/redhat-developer/mapt/pkg/util/logging" +) + +const ( + StackName = "stackOpenshiftRHOAI" + defaultPrefix = "rhoai" +) + +type RHOAIArgs struct { + KubeconfigPath string + Profiles []string + Prefix string +} + +type rhoaiRequest struct { + mCtx *mc.Context + prefix string + kubeconfigPath string + profiles []string +} + +func Create(mCtxArgs *mc.ContextArgs, args *RHOAIArgs) error { + mCtx, err := mc.Init(mCtxArgs, openshift.Provider()) + if err != nil { + return err + } + if err := profile.Validate(args.Profiles); err != nil { + return err + } + prefix := args.Prefix + if prefix == "" { + prefix = defaultPrefix + } + r := &rhoaiRequest{ + mCtx: mCtx, + prefix: prefix, + kubeconfigPath: args.KubeconfigPath, + profiles: args.Profiles, + } + return r.deploy() +} + +func Destroy(mCtxArgs *mc.ContextArgs) error { + logging.Debug("Run openshift rhoai destroy") + mCtx, err := mc.Init(mCtxArgs, openshift.Provider()) + if err != nil { + return err + } + return openshift.DestroyStack(mCtx, StackName) +} + +func (r *rhoaiRequest) deploy() error { + cs := manager.Stack{ + StackName: r.mCtx.StackNameByProject(StackName), + ProjectName: r.mCtx.ProjectName(), + BackedURL: r.mCtx.BackedURL(), + ProviderCredentials: openshift.NoCredentials, + DeployFunc: r.pulumiProgram, + } + _, err := manager.UpStack(r.mCtx, cs) + if err != nil { + return fmt.Errorf("stack creation failed: %w", err) + } + return nil +} + +func (r *rhoaiRequest) pulumiProgram(ctx *pulumi.Context) error { + kcBytes, err := os.ReadFile(r.kubeconfigPath) + if err != nil { + return fmt.Errorf("reading kubeconfig from %s: %w", r.kubeconfigPath, err) + } + kubeconfig := pulumi.String(string(kcBytes)).ToStringOutput() + + k8sProvider, err := profile.NewK8sProvider(ctx, "k8s-provider", kubeconfig) + if err != nil { + return err + } + + return profile.Deploy(ctx, r.profiles, &profile.DeployArgs{ + K8sProvider: k8sProvider, + Kubeconfig: kubeconfig, + Prefix: r.prefix, + }) +} diff --git a/pkg/provider/openshift/openshift.go b/pkg/provider/openshift/openshift.go new file mode 100644 index 000000000..76ac6dbc4 --- /dev/null +++ b/pkg/provider/openshift/openshift.go @@ -0,0 +1,36 @@ +package openshift + +import ( + "context" + + "github.com/redhat-developer/mapt/pkg/manager" + mc "github.com/redhat-developer/mapt/pkg/manager/context" + "github.com/redhat-developer/mapt/pkg/manager/credentials" +) + +type OpenShift struct{} + +func (o *OpenShift) Init(_ context.Context, _ string) (string, error) { + return "", nil +} + +func (o *OpenShift) DefaultHostingPlace() (*string, error) { + return nil, nil +} + +func Provider() *OpenShift { + return &OpenShift{} +} + +var NoCredentials = credentials.ProviderCredentials{} + +func DestroyStack(mCtx *mc.Context, stackName string) error { + return manager.DestroyStack( + mCtx, + manager.Stack{ + StackName: mCtx.StackNameByProject(stackName), + ProjectName: mCtx.ProjectName(), + BackedURL: mCtx.BackedURL(), + ProviderCredentials: NoCredentials, + }) +} diff --git a/pkg/target/service/snc/profile/client.go b/pkg/provider/openshift/profile/client.go similarity index 100% rename from pkg/target/service/snc/profile/client.go rename to pkg/provider/openshift/profile/client.go diff --git a/pkg/target/service/snc/profile/nfd.go b/pkg/provider/openshift/profile/nfd.go similarity index 100% rename from pkg/target/service/snc/profile/nfd.go rename to pkg/provider/openshift/profile/nfd.go diff --git a/pkg/target/service/snc/profile/nvidia.go b/pkg/provider/openshift/profile/nvidia.go similarity index 100% rename from pkg/target/service/snc/profile/nvidia.go rename to pkg/provider/openshift/profile/nvidia.go diff --git a/pkg/target/service/snc/profile/openshift_ai.go b/pkg/provider/openshift/profile/openshift_ai.go similarity index 100% rename from pkg/target/service/snc/profile/openshift_ai.go rename to pkg/provider/openshift/profile/openshift_ai.go diff --git a/pkg/target/service/snc/profile/operator.go b/pkg/provider/openshift/profile/operator.go similarity index 100% rename from pkg/target/service/snc/profile/operator.go rename to pkg/provider/openshift/profile/operator.go diff --git a/pkg/target/service/snc/profile/profile.go b/pkg/provider/openshift/profile/profile.go similarity index 98% rename from pkg/target/service/snc/profile/profile.go rename to pkg/provider/openshift/profile/profile.go index a7f7f59f4..1129472be 100644 --- a/pkg/target/service/snc/profile/profile.go +++ b/pkg/provider/openshift/profile/profile.go @@ -70,7 +70,7 @@ type DeployArgs struct { func Validate(profiles []string) error { for _, p := range profiles { if _, ok := profileRegistry[p]; !ok { - return fmt.Errorf("profile %q is not supported for SNC. Supported profiles: %v", + return fmt.Errorf("profile %q is not supported. Supported profiles: %v", p, slices.Sorted(maps.Keys(profileRegistry))) } } diff --git a/pkg/target/service/snc/profile/serverless.go b/pkg/provider/openshift/profile/serverless.go similarity index 100% rename from pkg/target/service/snc/profile/serverless.go rename to pkg/provider/openshift/profile/serverless.go diff --git a/pkg/target/service/snc/profile/servicemesh.go b/pkg/provider/openshift/profile/servicemesh.go similarity index 100% rename from pkg/target/service/snc/profile/servicemesh.go rename to pkg/provider/openshift/profile/servicemesh.go diff --git a/pkg/target/service/snc/profile/servicemesh_v2.go b/pkg/provider/openshift/profile/servicemesh_v2.go similarity index 100% rename from pkg/target/service/snc/profile/servicemesh_v2.go rename to pkg/provider/openshift/profile/servicemesh_v2.go diff --git a/pkg/target/service/snc/profile/virtualization.go b/pkg/provider/openshift/profile/virtualization.go similarity index 100% rename from pkg/target/service/snc/profile/virtualization.go rename to pkg/provider/openshift/profile/virtualization.go From 132edf343465bfa247e99ec48cd843cb7bfff894 Mon Sep 17 00:00:00 2001 From: Amos Mastbaum Date: Fri, 3 Jul 2026 08:16:58 +0300 Subject: [PATCH 2/2] fix(openshift): add NetworkPolicy for webhook access in mesh-enrolled namespaces When ServiceMesh enrolls knative-serving via SMMR, it creates a deny-all NetworkPolicy that blocks API server -> webhook traffic on multi-node clusters. This causes KnativeServing install to fail with webhook timeout errors. On SNC this was masked because all traffic is node-local. Add a NetworkPolicy allowing ingress to webhook pods on port 8443 before creating the Knative CR, so admission webhooks remain reachable regardless of mesh network policies. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/provider/openshift/profile/serverless.go | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/provider/openshift/profile/serverless.go b/pkg/provider/openshift/profile/serverless.go index e3f01f218..9fbabc41e 100644 --- a/pkg/provider/openshift/profile/serverless.go +++ b/pkg/provider/openshift/profile/serverless.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions" + networkingv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/networking/v1" metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "k8s.io/apimachinery/pkg/runtime/schema" @@ -100,6 +101,36 @@ func deployKnativeCR(ctx *pulumi.Context, args *DeployArgs, operatorReady pulumi return nil, pulumi.StringOutput{}, err } + // When ServiceMesh enrolls this namespace, it creates a deny-all + // NetworkPolicy that blocks API server → webhook traffic on multi-node + // clusters. Allow ingress to webhook pods so admission webhooks work. + if _, err := networkingv1.NewNetworkPolicy(ctx, rn(cr.suffix+"-webhook-np"), + &networkingv1.NetworkPolicyArgs{ + Metadata: &metav1.ObjectMetaArgs{ + Name: pulumi.Sprintf("allow-webhook-%s", cr.suffix), + Namespace: pulumi.String(cr.namespace), + }, + Spec: &networkingv1.NetworkPolicySpecArgs{ + PodSelector: &metav1.LabelSelectorArgs{ + MatchLabels: pulumi.StringMap{"app": pulumi.String("webhook")}, + }, + Ingress: networkingv1.NetworkPolicyIngressRuleArray{ + &networkingv1.NetworkPolicyIngressRuleArgs{ + Ports: networkingv1.NetworkPolicyPortArray{ + &networkingv1.NetworkPolicyPortArgs{ + Port: pulumi.Int(8443), + Protocol: pulumi.String("TCP"), + }, + }, + }, + }, + PolicyTypes: pulumi.StringArray{pulumi.String("Ingress")}, + }, + }, + args.k8sOpts(pulumi.DependsOn([]pulumi.Resource{ns}))...); err != nil { + return nil, pulumi.StringOutput{}, err + } + // Create the Knative CR res, err := apiextensions.NewCustomResource(ctx, rn(cr.suffix), &apiextensions.CustomResourceArgs{