Skip to content

Commit 49d12c6

Browse files
committed
feat(profile): add --operator-channel and --catalog-source flags
Allow overriding the OLM subscription channel and catalog source per operator when creating SNC clusters. This lets QE teams test specific operator versions or custom index images without modifying profile code. Validation runs before any infrastructure is provisioned to fail fast on invalid inputs.
1 parent ce74314 commit 49d12c6

6 files changed

Lines changed: 159 additions & 11 deletions

File tree

cmd/mapt/cmd/aws/services/snc.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ const (
2727

2828
sncProfile = "profile"
2929
sncProfileDesc = "comma separated list of profiles to apply on the SNC cluster. Profiles available: virtualization, serverless-serving, serverless-eventing, serverless, servicemesh, ai, nvidia. The ai profile automatically includes servicemesh and serverless-serving as prerequisites and raises the minimum instance size to 16 vCPUs. The nvidia profile installs NFD and the NVIDIA GPU Operator"
30+
31+
operatorChannel = "operator-channel"
32+
operatorChannelDesc = "override the OLM subscription channel for an operator (--operator-channel serverless-operator=preview,nfd=4.17)"
33+
catalogSource = "catalog-source"
34+
catalogSourceDesc = "override the OLM catalog source with a custom index image (--catalog-source serverless-operator=quay.io/my-org/my-index:latest)"
3035
)
3136

3237
func GetOpenshiftSNCCmd() *cobra.Command {
@@ -92,7 +97,9 @@ func createSNC() *cobra.Command {
9297
PullSecretFile: viper.GetString(pullSecretFile),
9398
Timeout: viper.GetString(params.Timeout),
9499
ServiceEndpoints: params.NetworkServiceEndpoints(),
95-
Profiles: profiles}); err != nil {
100+
Profiles: profiles,
101+
OperatorChannels: viper.GetStringMapString(operatorChannel),
102+
CatalogSources: viper.GetStringMapString(catalogSource)}); err != nil {
96103
return err
97104
}
98105
return nil
@@ -107,6 +114,8 @@ func createSNC() *cobra.Command {
107114
flagSet.StringP(params.Timeout, "", "", params.TimeoutDesc)
108115
flagSet.StringToStringP(params.Tags, "", nil, params.TagsDesc)
109116
flagSet.StringSliceP(sncProfile, "", []string{}, sncProfileDesc)
117+
flagSet.StringToStringP(operatorChannel, "", nil, operatorChannelDesc)
118+
flagSet.StringToStringP(catalogSource, "", nil, catalogSourceDesc)
110119
params.AddComputeRequestFlags(flagSet)
111120
params.AddSpotFlags(flagSet)
112121
params.AddNetworkFlags(flagSet, awsParams.ServiceEndpointsDesc)

docs/aws/openshift-snc.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,56 @@ Multiple profiles can be specified as a comma-separated list (e.g., `--profile v
7777
| `nvidia` | Installs the [NVIDIA GPU Operator](https://docs.nvidia.com/datacenter/cloud-native/openshift/latest/install-gpu-ocp.html) on the cluster. Automatically installs [Node Feature Discovery](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/specialized_hardware_and_driver_enablement/psap-node-feature-discovery-operator) (NFD) as a prerequisite and creates a ClusterPolicy with the recommended OpenShift defaults (CRI-O runtime, OCP driver toolkit). The cluster must run on a GPU-capable instance type (e.g. `g4dn`, `g5`, `p4d`).|
7878

7979

80+
### Operator overrides
81+
82+
Profiles install operators using the default OLM channel (`stable`) and catalog (`redhat-operators`). Two flags allow overriding these per operator, which is useful for testing pre-release operator builds:
83+
84+
#### `--operator-channel`
85+
86+
Override the OLM subscription channel for a specific operator:
87+
88+
```bash
89+
mapt aws openshift-snc create \
90+
--profile serverless-serving \
91+
--operator-channel serverless-operator=candidate
92+
```
93+
94+
Multiple operators can be overridden at once:
95+
96+
```bash
97+
--operator-channel serverless-operator=preview,nfd=4.17
98+
```
99+
100+
#### `--catalog-source`
101+
102+
Use a custom index image instead of the default catalog. This creates a `CatalogSource` CR in `openshift-marketplace` and points the operator's subscription to it:
103+
104+
```bash
105+
mapt aws openshift-snc create \
106+
--profile nvidia \
107+
--catalog-source gpu-operator-certified=quay.io/my-team/gpu-operator-index:test-v1.0
108+
```
109+
110+
Both flags can be combined:
111+
112+
```bash
113+
mapt aws openshift-snc create \
114+
--profile ai \
115+
--operator-channel serverless-operator=candidate \
116+
--catalog-source rhods-operator=quay.io/my-team/rhoai-index:nightly
117+
```
118+
119+
When neither flag is provided, operators use the defaults: channel `stable` and catalog `redhat-operators` (unless overridden in the profile definition, e.g. `gpu-operator-certified` and `nfd` use `certified-operators`).
120+
121+
The keys are operator package names as they appear in OLM. The operators installed by each profile are:
122+
123+
| Profile | Operator package names |
124+
|---------|----------------------|
125+
| `serverless-serving` / `serverless-eventing` / `serverless` | `serverless-operator` |
126+
| `servicemesh` | `servicemeshoperator3` |
127+
| `ai` | `rhods-operator`, `servicemeshoperator`, `authorino-operator`, `serverless-operator` |
128+
| `nvidia` | `gpu-operator-certified`, `nfd` |
129+
80130
### Adding new profiles
81131

82132
To add a new profile:

pkg/provider/aws/action/snc/snc.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ type openshiftSNCRequest struct {
4545
pullSecretFile *string
4646
serviceEndpoints []string
4747
allocationData *allocation.AllocationResult
48-
profiles []string
49-
diskSize *int
48+
profiles []string
49+
operatorChannels map[string]string
50+
catalogSources map[string]string
51+
diskSize *int
5052
}
5153

5254
func (r *openshiftSNCRequest) validate() error {
@@ -67,10 +69,13 @@ func Create(mCtxArgs *mc.ContextArgs, args *apiSNC.SNCArgs) (_ *apiSNC.SNCResult
6769
if err != nil {
6870
return nil, err
6971
}
70-
// Validate profiles
72+
// Validate profiles and operator overrides
7173
if err := profile.Validate(args.Profiles); err != nil {
7274
return nil, err
7375
}
76+
if err := profile.ValidateOperatorOverrides(args.OperatorChannels, args.CatalogSources); err != nil {
77+
return nil, err
78+
}
7479
// Compose request
7580
prefix := util.If(len(args.Prefix) > 0, args.Prefix, "main")
7681
r := openshiftSNCRequest{
@@ -82,8 +87,10 @@ func Create(mCtxArgs *mc.ContextArgs, args *apiSNC.SNCArgs) (_ *apiSNC.SNCResult
8287
pullSecretFile: &args.PullSecretFile,
8388
timeout: &args.Timeout,
8489
serviceEndpoints: args.ServiceEndpoints,
85-
profiles: args.Profiles,
86-
diskSize: args.ComputeRequest.DiskSize}
90+
profiles: args.Profiles,
91+
operatorChannels: args.OperatorChannels,
92+
catalogSources: args.CatalogSources,
93+
diskSize: args.ComputeRequest.DiskSize}
8794
if args.Spot != nil {
8895
r.spot = args.Spot.Spot
8996
}
@@ -290,10 +297,12 @@ func (r *openshiftSNCRequest) deploy(ctx *pulumi.Context) error {
290297
deletedWith = c.AutoscalingGroup
291298
}
292299
if err := profile.Deploy(ctx, r.profiles, &profile.DeployArgs{
293-
K8sProvider: k8sProvider,
294-
Kubeconfig: kubeconfig,
295-
Prefix: *r.prefix,
296-
DeletedWith: deletedWith,
300+
K8sProvider: k8sProvider,
301+
Kubeconfig: kubeconfig,
302+
Prefix: *r.prefix,
303+
DeletedWith: deletedWith,
304+
OperatorChannels: r.operatorChannels,
305+
CatalogSources: r.catalogSources,
297306
}); err != nil {
298307
return err
299308
}

pkg/target/service/snc/api.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ type SNCArgs struct {
5050
Spot *spotTypes.SpotArgs
5151
Timeout string
5252
ServiceEndpoints []string
53-
Profiles []string
53+
Profiles []string
54+
OperatorChannels map[string]string
55+
CatalogSources map[string]string
5456
}
5557

5658
type SNCResults struct {

pkg/target/service/snc/profile/operator.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,18 @@ func installOperator(ctx *pulumi.Context, args *DeployArgs, oi operatorInstall)
5959
catalogSource = catalogSourceRedHat
6060
}
6161

62+
if override, ok := args.OperatorChannels[oi.packageName]; ok {
63+
channel = override
64+
}
65+
if cs, ok := args.catalogSourceCRs[oi.packageName]; ok {
66+
catalogSource = cs.Name
67+
}
68+
6269
deps := append([]pulumi.Resource{}, args.Deps...)
6370
deps = append(deps, oi.extraDeps...)
71+
if cs, ok := args.catalogSourceCRs[oi.packageName]; ok {
72+
deps = append(deps, cs.Resource)
73+
}
6474

6575
// If ogName is provided, create a dedicated namespace and OperatorGroup.
6676
if oi.ogName != "" {

pkg/target/service/snc/profile/profile.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package profile
22

33
import (
4+
"crypto/sha256"
45
"fmt"
56
"maps"
67
"slices"
78

89
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
10+
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions"
911
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
1012
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
1113
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
@@ -63,6 +65,18 @@ type DeployArgs struct {
6365
// so that Pulumi skips deleting them individually during destroy — the
6466
// resources disappear when the VM is terminated.
6567
DeletedWith pulumi.Resource
68+
// OperatorChannels maps operator packageName to an OLM channel override.
69+
OperatorChannels map[string]string
70+
// CatalogSources maps operator packageName to a custom index image URL.
71+
CatalogSources map[string]string
72+
73+
// catalogSourceCRs maps packageName to the CatalogSource CR info.
74+
catalogSourceCRs map[string]catalogSourceInfo
75+
}
76+
77+
type catalogSourceInfo struct {
78+
Name string
79+
Resource pulumi.Resource
6680
}
6781

6882
// Validate checks that all requested profiles are supported and
@@ -88,6 +102,10 @@ func Validate(profiles []string) error {
88102
// The AI profile implicitly brings in Service Mesh v2 (Maistra) and
89103
// serverless-serving as prerequisites for Kserve.
90104
func Deploy(ctx *pulumi.Context, profiles []string, args *DeployArgs) error {
105+
if err := args.ensureCatalogSources(ctx); err != nil {
106+
return err
107+
}
108+
91109
needServing := false
92110
needEventing := false
93111
needAI := false
@@ -194,6 +212,56 @@ func (a *DeployArgs) newNamespace(ctx *pulumi.Context, name string, nsName pulum
194212
a.k8sOpts(extra...)...)
195213
}
196214

215+
func ValidateOperatorOverrides(channels, catalogs map[string]string) error {
216+
for pkg, ch := range channels {
217+
if pkg == "" || ch == "" {
218+
return fmt.Errorf("invalid --operator-channel: both package name and channel must be non-empty (got %q=%q)", pkg, ch)
219+
}
220+
}
221+
for pkg, img := range catalogs {
222+
if pkg == "" || img == "" {
223+
return fmt.Errorf("invalid --catalog-source: both package name and index image must be non-empty (got %q=%q)", pkg, img)
224+
}
225+
}
226+
return nil
227+
}
228+
229+
// ensureCatalogSources creates CatalogSource CRs for any custom index images
230+
// specified via --catalog-source, so that operator subscriptions can reference them.
231+
func (a *DeployArgs) ensureCatalogSources(ctx *pulumi.Context) error {
232+
if len(a.CatalogSources) == 0 {
233+
return nil
234+
}
235+
a.catalogSourceCRs = make(map[string]catalogSourceInfo, len(a.CatalogSources))
236+
for pkg, indexImage := range a.CatalogSources {
237+
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(indexImage)))[:8]
238+
csName := fmt.Sprintf("mapt-cs-%s-%s", pkg, hash)
239+
cs, err := apiextensions.NewCustomResource(ctx, csName,
240+
&apiextensions.CustomResourceArgs{
241+
ApiVersion: pulumi.String("operators.coreos.com/v1alpha1"),
242+
Kind: pulumi.String("CatalogSource"),
243+
Metadata: &metav1.ObjectMetaArgs{
244+
Name: pulumi.String(csName),
245+
Namespace: pulumi.String("openshift-marketplace"),
246+
},
247+
OtherFields: map[string]interface{}{
248+
"spec": map[string]interface{}{
249+
"sourceType": "grpc",
250+
"image": indexImage,
251+
"displayName": fmt.Sprintf("MAPT custom catalog for %s", pkg),
252+
"publisher": "MAPT",
253+
},
254+
},
255+
},
256+
a.k8sOpts(pulumi.DependsOn(a.Deps))...)
257+
if err != nil {
258+
return err
259+
}
260+
a.catalogSourceCRs[pkg] = catalogSourceInfo{Name: csName, Resource: cs}
261+
}
262+
return nil
263+
}
264+
197265
// k8sOpts returns the common Pulumi resource options for K8s resources:
198266
// the K8s provider and (when set) the DeletedWith option. Extra options
199267
// (e.g. DependsOn) can be appended.

0 commit comments

Comments
 (0)