Skip to content

Commit f7504a9

Browse files
committed
feat(snc): add servicemesh profile with OpenShift Service Mesh 3 support
Installs the Red Hat OpenShift Service Mesh 3 operator (servicemeshoperator3) via OLM and deploys cluster-scoped IstioCNI and Istio CRs using the sailoperator.io/v1 API. Also updates findResource to support cluster-scoped resource lookups when namespace is empty.
1 parent c283c05 commit f7504a9

5 files changed

Lines changed: 202 additions & 4 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
disableClusterReadinessDesc = "If this flag is set it will skip the checks for the cluster readiness. In this case the kubeconfig can not be generated"
2525

2626
sncProfile = "profile"
27-
sncProfileDesc = "comma separated list of profiles to apply on the SNC cluster. Profiles available: virtualization"
27+
sncProfileDesc = "comma separated list of profiles to apply on the SNC cluster. Profiles available: virtualization, servicemesh"
2828
)
2929

3030
func GetOpenshiftSNCCmd() *cobra.Command {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ func (r *openshiftSNCRequest) deploy(ctx *pulumi.Context) error {
264264
}
265265
ctx.Export(fmt.Sprintf("%s-%s", *r.prefix, apiSNC.OutputKubeconfig),
266266
pulumi.ToSecret(kubeconfig))
267+
// Write kubeconfig to disk early so it is available even if profile deployment fails
268+
if outputPath := r.mCtx.GetResultsOutputPath(); len(outputPath) > 0 {
269+
kubeconfig.ApplyT(func(kc string) error {
270+
return os.WriteFile(fmt.Sprintf("%s/kubeconfig", outputPath), []byte(kc), 0600)
271+
})
272+
}
267273
// Deploy profiles using Kubernetes provider
268274
if len(r.profiles) > 0 {
269275
k8sProvider, err := apiSNC.NewK8sProvider(ctx, "k8s-provider", kubeconfig)

pkg/target/service/snc/client.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,22 @@ func waitForCRCondition(ctx context.Context, kubeconfig string, gvr schema.Group
6969
}
7070

7171
// findResource returns a single resource by exact name or by name prefix.
72+
// When namespace is empty, the resource is looked up at cluster scope.
7273
func findResource(ctx context.Context, dc dynamic.Interface, gvr schema.GroupVersionResource,
7374
namespace, name string, prefixMatch bool) (*unstructured.Unstructured, error) {
7475

76+
var ri dynamic.ResourceInterface
77+
if namespace == "" {
78+
ri = dc.Resource(gvr)
79+
} else {
80+
ri = dc.Resource(gvr).Namespace(namespace)
81+
}
82+
7583
if !prefixMatch {
76-
return dc.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
84+
return ri.Get(ctx, name, metav1.GetOptions{})
7785
}
7886

79-
list, err := dc.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
87+
list, err := ri.List(ctx, metav1.ListOptions{})
8088
if err != nil {
8189
return nil, err
8290
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package snc
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions"
8+
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
9+
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
10+
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
11+
"k8s.io/apimachinery/pkg/runtime/schema"
12+
)
13+
14+
const (
15+
istioSystemNamespace = "istio-system"
16+
istioCNINamespace = "istio-cni"
17+
)
18+
19+
var (
20+
sailCSVGVR = schema.GroupVersionResource{
21+
Group: "operators.coreos.com",
22+
Version: "v1alpha1",
23+
Resource: "clusterserviceversions",
24+
}
25+
istioGVR = schema.GroupVersionResource{
26+
Group: "sailoperator.io",
27+
Version: "v1",
28+
Resource: "istios",
29+
}
30+
istioCNIGVR = schema.GroupVersionResource{
31+
Group: "sailoperator.io",
32+
Version: "v1",
33+
Resource: "istiocnis",
34+
}
35+
)
36+
37+
func deployServiceMesh(ctx *pulumi.Context, args *ProfileDeployArgs) (pulumi.Resource, error) {
38+
goCtx := ctx.Context()
39+
rn := func(suffix string) string {
40+
return fmt.Sprintf("%s-smesh-%s", args.Prefix, suffix)
41+
}
42+
43+
// Create istio-system namespace
44+
nsSystem, err := corev1.NewNamespace(ctx, rn("ns-system"),
45+
&corev1.NamespaceArgs{
46+
Metadata: &metav1.ObjectMetaArgs{
47+
Name: pulumi.String(istioSystemNamespace),
48+
},
49+
},
50+
pulumi.Provider(args.K8sProvider),
51+
pulumi.DependsOn(args.Deps))
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
// Create istio-cni namespace
57+
nsCNI, err := corev1.NewNamespace(ctx, rn("ns-cni"),
58+
&corev1.NamespaceArgs{
59+
Metadata: &metav1.ObjectMetaArgs{
60+
Name: pulumi.String(istioCNINamespace),
61+
},
62+
},
63+
pulumi.Provider(args.K8sProvider),
64+
pulumi.DependsOn(args.Deps))
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
// Create Subscription for the OpenShift Service Mesh 3 operator
70+
sub, err := apiextensions.NewCustomResource(ctx, rn("sub"),
71+
&apiextensions.CustomResourceArgs{
72+
ApiVersion: pulumi.String("operators.coreos.com/v1alpha1"),
73+
Kind: pulumi.String("Subscription"),
74+
Metadata: &metav1.ObjectMetaArgs{
75+
Name: pulumi.String("servicemeshoperator3"),
76+
Namespace: pulumi.String("openshift-operators"),
77+
},
78+
OtherFields: map[string]interface{}{
79+
"spec": map[string]interface{}{
80+
"source": "redhat-operators",
81+
"sourceNamespace": "openshift-marketplace",
82+
"name": "servicemeshoperator3",
83+
"channel": "stable",
84+
"installPlanApproval": "Automatic",
85+
},
86+
},
87+
},
88+
pulumi.Provider(args.K8sProvider),
89+
pulumi.DependsOn([]pulumi.Resource{nsSystem, nsCNI}))
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// Wait for the Service Mesh operator CSV to succeed
95+
csvReady := pulumi.All(sub.ID(), args.Kubeconfig).ApplyT(
96+
func(allArgs []interface{}) (string, error) {
97+
kc := allArgs[1].(string)
98+
if err := waitForCRCondition(goCtx, kc, sailCSVGVR,
99+
"openshift-operators", "servicemeshoperator3",
100+
"", "Succeeded", 20*time.Minute, true); err != nil {
101+
return "", fmt.Errorf("waiting for Service Mesh operator CSV: %w", err)
102+
}
103+
return "ready", nil
104+
}).(pulumi.StringOutput)
105+
106+
// Create IstioCNI CR
107+
istioCNIName := csvReady.ApplyT(func(_ string) string {
108+
return "default"
109+
}).(pulumi.StringOutput)
110+
111+
// IstioCNI is cluster-scoped
112+
cni, err := apiextensions.NewCustomResource(ctx, rn("istiocni"),
113+
&apiextensions.CustomResourceArgs{
114+
ApiVersion: pulumi.String("sailoperator.io/v1"),
115+
Kind: pulumi.String("IstioCNI"),
116+
Metadata: &metav1.ObjectMetaArgs{
117+
Name: istioCNIName,
118+
},
119+
OtherFields: map[string]interface{}{
120+
"spec": map[string]interface{}{
121+
"namespace": istioCNINamespace,
122+
"profile": "openshift",
123+
},
124+
},
125+
},
126+
pulumi.Provider(args.K8sProvider))
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
// Wait for IstioCNI to be ready (cluster-scoped, empty namespace)
132+
cniReady := pulumi.All(cni.ID(), args.Kubeconfig).ApplyT(
133+
func(allArgs []interface{}) (string, error) {
134+
kc := allArgs[1].(string)
135+
if err := waitForCRCondition(goCtx, kc, istioCNIGVR,
136+
"", "default",
137+
"Ready", "True", 20*time.Minute, false); err != nil {
138+
return "", fmt.Errorf("waiting for IstioCNI: %w", err)
139+
}
140+
return "ready", nil
141+
}).(pulumi.StringOutput)
142+
143+
// Create Istio CR (cluster-scoped, depends on CNI being ready)
144+
istioName := cniReady.ApplyT(func(_ string) string {
145+
return "default"
146+
}).(pulumi.StringOutput)
147+
148+
istio, err := apiextensions.NewCustomResource(ctx, rn("istio"),
149+
&apiextensions.CustomResourceArgs{
150+
ApiVersion: pulumi.String("sailoperator.io/v1"),
151+
Kind: pulumi.String("Istio"),
152+
Metadata: &metav1.ObjectMetaArgs{
153+
Name: istioName,
154+
},
155+
OtherFields: map[string]interface{}{
156+
"spec": map[string]interface{}{
157+
"namespace": istioSystemNamespace,
158+
},
159+
},
160+
},
161+
pulumi.Provider(args.K8sProvider))
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
// Wait for Istio to be ready (cluster-scoped, empty namespace)
167+
istioReady := pulumi.All(istio.ID(), args.Kubeconfig).ApplyT(
168+
func(allArgs []interface{}) (string, error) {
169+
kc := allArgs[1].(string)
170+
if err := waitForCRCondition(goCtx, kc, istioGVR,
171+
"", "default",
172+
"Ready", "True", 20*time.Minute, false); err != nil {
173+
return "", fmt.Errorf("waiting for Istio: %w", err)
174+
}
175+
return "ready", nil
176+
}).(pulumi.StringOutput)
177+
178+
ctx.Export("istioReady", istioReady)
179+
180+
return istio, nil
181+
}

pkg/target/service/snc/profiles.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import (
1010

1111
const (
1212
ProfileVirtualization = "virtualization"
13+
ProfileServiceMesh = "servicemesh"
1314
)
1415

1516
// validProfiles is the single source of truth for supported profile names.
16-
var validProfiles = []string{ProfileVirtualization}
17+
var validProfiles = []string{ProfileVirtualization, ProfileServiceMesh}
1718

1819
// ProfileDeployArgs holds the arguments needed by a profile to deploy
1920
// its resources on the SNC cluster.
@@ -40,6 +41,8 @@ func DeployProfile(ctx *pulumi.Context, profile string, args *ProfileDeployArgs)
4041
switch profile {
4142
case ProfileVirtualization:
4243
return deployVirtualization(ctx, args)
44+
case ProfileServiceMesh:
45+
return deployServiceMesh(ctx, args)
4346
default:
4447
return nil, fmt.Errorf("profile %q has no deploy function", profile)
4548
}

0 commit comments

Comments
 (0)