diff --git a/core-istio-mesh/charts/istio-components/Chart.yaml b/core-istio-mesh/charts/istio-components/Chart.yaml index 9c28e5f..c0b3d3c 100644 --- a/core-istio-mesh/charts/istio-components/Chart.yaml +++ b/core-istio-mesh/charts/istio-components/Chart.yaml @@ -2,4 +2,8 @@ apiVersion: v2 name: istio-components version: 0.1.0 description: A Helm chart for deploying gateway(s) and related components -type: application \ No newline at end of file +type: application +dependencies: + - name: coretpl + version: "0.2.3" + repository: "https://netcracker.github.io/qubership-core-bootstrap/" \ No newline at end of file diff --git a/core-istio-mesh/charts/istio-components/declarations/services.yaml b/core-istio-mesh/charts/istio-components/declarations/services.yaml new file mode 100644 index 0000000..52ab89c --- /dev/null +++ b/core-istio-mesh/charts/istio-components/declarations/services.yaml @@ -0,0 +1,62 @@ +--- +kind: Service +apiVersion: v1 +metadata: + name: '{{ .Values.PUBLIC_GW_SERVICE_NAME }}' + annotations: + gateway.target: '{{ .Values.ISTIO_PUBLIC_GATEWAY_NAME }}' + gateway.route: '{{ .Values.PUBLIC_GW_ROUTE_NAME }}' + labels: + name: '{{ .Values.PUBLIC_GW_SERVICE_NAME }}' + app.kubernetes.io/name: '{{ .Values.PUBLIC_GW_SERVICE_NAME }}' + app.kubernetes.io/part-of: 'Cloud-Core' + app.kubernetes.io/managed-by: Helm + deployment.qubership.org/sessionId: '{{ .Values.DEPLOYMENT_SESSION_ID }}' +spec: + ports: + - name: web + protocol: TCP + port: 8080 + targetPort: 8080 + selector: + name: '{{ .Values.ISTIO_PUBLIC_GATEWAY_NAME }}' + clusterIP: None + clusterIPs: + - None + type: ClusterIP + sessionAffinity: None + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + internalTrafficPolicy: Cluster +--- +kind: Service +apiVersion: v1 +metadata: + name: '{{ .Values.PRIVATE_GW_SERVICE_NAME }}' + annotations: + gateway.target: '{{ .Values.ISTIO_PRIVATE_GATEWAY_NAME }}' + gateway.route: '{{ .Values.PRIVATE_GW_ROUTE_NAME }}' + labels: + name: '{{ .Values.PRIVATE_GW_SERVICE_NAME }}' + app.kubernetes.io/name: '{{ .Values.PRIVATE_GW_SERVICE_NAME }}' + app.kubernetes.io/part-of: 'Cloud-Core' + app.kubernetes.io/managed-by: Helm + deployment.qubership.org/sessionId: '{{ .Values.DEPLOYMENT_SESSION_ID }}' +spec: + ports: + - name: web + protocol: TCP + port: 8080 + targetPort: 8080 + selector: + name: '{{ .Values.ISTIO_PRIVATE_GATEWAY_NAME }}' + clusterIP: None + clusterIPs: + - None + type: ClusterIP + sessionAffinity: None + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + internalTrafficPolicy: Cluster \ No newline at end of file diff --git a/core-istio-mesh/charts/istio-components/templates/corebootstrap.yaml b/core-istio-mesh/charts/istio-components/templates/corebootstrap.yaml new file mode 100644 index 0000000..bf790f2 --- /dev/null +++ b/core-istio-mesh/charts/istio-components/templates/corebootstrap.yaml @@ -0,0 +1 @@ +{{ include "coretpl.synchronizer.hooks" . }} diff --git a/core-istio-mesh/charts/istio-components/templates/routes.yaml b/core-istio-mesh/charts/istio-components/templates/routes.yaml index 8e1f6fa..f1ed336 100644 --- a/core-istio-mesh/charts/istio-components/templates/routes.yaml +++ b/core-istio-mesh/charts/istio-components/templates/routes.yaml @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: fallback-to-private-gateway + name: '{{ .Values.PRIVATE_GW_ROUTE_NAME }}' spec: parentRefs: - name: '{{ .Values.ISTIO_PRIVATE_GATEWAY_NAME }}' @@ -17,7 +17,7 @@ spec: apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: fallback-to-public-gateway + name: '{{ .Values.PUBLIC_GW_ROUTE_NAME }}' spec: parentRefs: - name: '{{ .Values.ISTIO_PUBLIC_GATEWAY_NAME }}' diff --git a/core-istio-mesh/charts/istio-components/values.yaml b/core-istio-mesh/charts/istio-components/values.yaml index 48428d4..de6cc1e 100644 --- a/core-istio-mesh/charts/istio-components/values.yaml +++ b/core-istio-mesh/charts/istio-components/values.yaml @@ -1,4 +1,10 @@ ISTIO_PRIVATE_GATEWAY_NAME: private-gateway ISTIO_PUBLIC_GATEWAY_NAME: public-gateway PUBLIC_FB_SERVICE_NAME: public-fallback-service -PRIVATE_FB_SERVICE_NAME: private-fallback-service \ No newline at end of file +PRIVATE_FB_SERVICE_NAME: private-fallback-service +PRIVATE_GW_SERVICE_NAME: private-gateway-service +PUBLIC_GW_SERVICE_NAME: public-gateway-service +PRIVATE_GW_ROUTE_NAME: fallback-to-private-gateway +PUBLIC_GW_ROUTE_NAME: fallback-to-public-gateway +CHECK_ISTIO_INTERGATION: "true" +CHECK_DECLARATION_PLURALS: "services,gateways" \ No newline at end of file diff --git a/coretpl/templates/_synchronizer.yaml b/coretpl/templates/_synchronizer.yaml index 956a3df..b7a4597 100644 --- a/coretpl/templates/_synchronizer.yaml +++ b/coretpl/templates/_synchronizer.yaml @@ -71,6 +71,8 @@ spec: value: {{ .Values.DEPLOYMENT_SESSION_ID }} - name: DECLARATIONS_PLURALS value: "{{ .Values.CHECK_DECLARATION_PLURALS }}" + - name: ISTIO_INTERGATION + value: "{{ .Values.CHECK_ISTIO_INTERGATION | default false }}" restartPolicy: Never serviceAccountName: finalyzer-user {{ $filesExist := (.Files.Glob "declarations/*") }} diff --git a/cr-synchronizer/README.md b/cr-synchronizer/README.md index e69de29..2466134 100644 --- a/cr-synchronizer/README.md +++ b/cr-synchronizer/README.md @@ -0,0 +1,92 @@ +# cr-synchronizer + +cr-synchronizer is a lightweight controller component that watches and synchronizes Custom Resources (CRs) used by the qubership platform. It ensures desired CR state is propagated, reconciled, and recorded with structured events and labels. + +## Istio gateway handling +For correct routing through Istio Gateway and ensure backward compatibility were kept, fallback routes have been included. After installing, we need to update existing services to switch to these fallback routes accordingly. So the gateway_service_generator was added to check preconditions before services switching + +### Local development + +Place the files with the desired Service manifests into the chart's `declarations/` so that the Helm expression `.Files.Glob "declarations/*"` in `_synchronizer.yaml` includes them in a ConfigMap. +Set or pass the values used by the template: `CR_SYNCHRONIZER_IMAGE`, `CHECK_ISTIO_INTERGRATION`, `DEPLOYMENT_SESSION_ID`, `CHECK_DECLARATION_PLURALS` (if needed), `SERVICE_NAME`, etc. + +Example `values.yaml` (minimum for startup): + +```yaml +CR_SYNCHRONIZER_IMAGE: "your-registry/cr-synchronizer:latest" + +# Enable Istio-check step inside synchronizer job +CHECK_ISTIO_INTERGATION: true + +# Session id / service name used by the template +DEPLOYMENT_SESSION_ID: "postdeploy-{{ .Release.Revision }}" +SERVICE_NAME: "my-service" +APPLICATION_NAME: "my-app" + +# Optional: list of plurals to process +CHECK_DECLARATION_PLURALS: "services,gateways" +RESOURCE_POLLING_TIMEOUT: 300 +``` + +**How to organize declaratives (chart structure):** + +Create a `declarations/` folder in your chart, and add YAML files there (each file can contain one or multiple objects separated by `---`). +The `_synchronizer.yaml` template already does: +- `{{ $filesExist := (.Files.Glob "declarations/*") }}` — if files exist, it creates a ConfigMap named `synchronizer.transport.configmap` and includes all files as data entries. +- The `synchronizer.preinstall.job` mounts this ConfigMap into the container: the volume `declarations-{{ .Values.SERVICE_NAME }}` is mounted at `/mnt/declaratives`. + +**Example declaration file (`declarations/my-gateway-services.yaml`):** + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: my-service + annotations: + gateway.target: "my-gateway-name" + gateway.route: "my-route-name" +spec: + selector: + app: my-app + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +``` +where gateway.target and gateway.route are preconditions to process services declarations. These resources must be deployed first + +**Installing the chart (example):** + +```bash +helm upgrade --install my-app ./my-chart \ + --set CR_SYNCHRONIZER_IMAGE=cr-synchronizer:latest \ + --set CHECK_ISTIO_INTERGATION=true \ + --set DEPLOYMENT_SESSION_ID="session-123" \ + --set SERVICE_NAME="my-service" \ + --namespace my-controller-namespace +``` + +**Verification after installation:** + +Make sure the desired Gateway and HTTPRoute exist in the namespace: + +```bash +# adjust resource names accordingly +kubectl -n my-controller-namespace get gateway,myroute +kubectl -n my-controller-namespace get httproute +``` + +Check logs of the Job/Pod for debugging: + +```bash +kubectl -n my-controller-namespace logs job/synchronizer-preinstall-job-name +# or, if it's a Deployment/Pod: +kubectl -n my-controller-namespace logs deploy/my-cr-synchronizer +``` + +Verify that the service has been applied: + +```bash +kubectl -n my-controller-namespace get svc my-service +``` diff --git a/cr-synchronizer/getters/deploymentGenerator.go b/cr-synchronizer/getters/deploymentGenerator.go index 4448b9a..7539412 100644 --- a/cr-synchronizer/getters/deploymentGenerator.go +++ b/cr-synchronizer/getters/deploymentGenerator.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "sync" "time" @@ -165,6 +166,12 @@ func (ng *DeploymentGenerator) createKnownGeneratorManager(dcl map[string][]unst } generatorManager.register(NewMaaSesRunnerGenerator(ng.ctx, dcl[MaaSKind], ng.client, ng.recorder, ng.clientset, ng.scheme, ng.runtimeReceiver, ng.timeoutSeconds)) generatorManager.register(NewDBaaSesRunnerGenerator(ng.ctx, dcl[DBaaSKind], ng.client, ng.recorder, ng.clientset, ng.scheme, ng.runtimeReceiver, ng.timeoutSeconds)) + + if val, ok := os.LookupEnv("ISTIO_INTERGATION"); ok && strings.EqualFold(val, "true") { + generatorManager.register(NewGatewayServiceGenerator(ng.ctx, dcl["Service"], ng.client, ng.timeoutSeconds)) + } else { + log.Info().Str("type", "init").Msg("ISTIO_INTERGATION not enabled; skipping GatewayServiceGenerator registration") + } return generatorManager } diff --git a/cr-synchronizer/getters/gateway_service_generator.go b/cr-synchronizer/getters/gateway_service_generator.go new file mode 100644 index 0000000..ea2cabc --- /dev/null +++ b/cr-synchronizer/getters/gateway_service_generator.go @@ -0,0 +1,149 @@ +package getters + +import ( + "context" + "fmt" + "os" + "strings" + + k8sv1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +const ( + gatewayGVRGroup = "gateway.networking.k8s.io" + gatewayGVRVersion = "v1" + gatewayGVRResource = "gateways" + httpRouteResource = "httproutes" +) + +// GatewayServiceGenerator applies Service resources conditionally when a Gateway and a referencing HTTPRoute exist. +type GatewayServiceGenerator struct { + ctx context.Context + client dynamic.Interface + services []unstructured.Unstructured + timeoutSeconds int +} + +func NewGatewayServiceGenerator(ctx context.Context, services []unstructured.Unstructured, client dynamic.Interface, timeoutSeconds int) *GatewayServiceGenerator { + return &GatewayServiceGenerator{ + ctx: ctx, + client: client, + services: services, + timeoutSeconds: timeoutSeconds, + } +} + +func (g *GatewayServiceGenerator) Name() string { return "gatewayServiceApplier" } + +func (g *GatewayServiceGenerator) Generate() { + if val, ok := os.LookupEnv("ISTIO_INTERGATION"); !ok || !strings.EqualFold(val, "true") { + log.Info().Str("type", "gatewayServiceApplier").Msg("ISTIO_INTERGATION not enabled; skipping gateway service application") + return + } + + if len(g.services) == 0 { + log.Info().Str("type", "gatewayServiceApplier").Msg("no Service objects provided in declaratives, skipping") + return + } + + for _, svc := range g.services { + annotations := svc.GetAnnotations() + if annotations == nil { + log.Info().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Msg("service has no annotations, skipping") + continue + } + + // Annotation keys expected: gateway.target and gateway.route + gatewayName := strings.TrimSpace(annotations["gateway.target"]) + routeName := strings.TrimSpace(annotations["gateway.route"]) + if gatewayName == "" || routeName == "" { + log.Info().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Msg("required annotations gateway.target or gateway.route missing, skipping") + continue + } + + available, err := IsGatewayAndRoutePresent(g.ctx, g.client, gatewayName, routeName) + if err != nil { + log.Warn().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Err(err).Msg("error checking gateway/route availability, skipping") + continue + } + if !available { + log.Info().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Msg("gateway/route not available, skipping") + continue + } + + svcGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + + customLabels := svc.GetLabels() + if customLabels == nil { + customLabels = make(map[string]string) + } + customLabels["app.kubernetes.io/managed-by"] = manager + svc.SetLabels(customLabels) + + _, err = g.client.Resource(svcGVR).Namespace(namespace).Get(g.ctx, svc.GetName(), k8sv1.GetOptions{}) + if err != nil { + // create + _, err := g.client.Resource(svcGVR).Namespace(namespace).Create(g.ctx, &svc, k8sv1.CreateOptions{FieldManager: "pre-hook"}) + if err != nil { + log.Warn().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Err(err).Msg("failed to create service") + continue + } + log.Info().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Msg("service created") + } else { + // update + svc.SetResourceVersion("") + _, err := g.client.Resource(svcGVR).Namespace(namespace).Update(g.ctx, &svc, k8sv1.UpdateOptions{FieldManager: "pre-hook"}) + if err != nil { + log.Warn().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Err(err).Msg("failed to update service") + continue + } + log.Info().Str("type", "gatewayServiceApplier").Str("service", svc.GetName()).Msg("service updated") + } + } +} + +// IsGatewayAndRoutePresent checks whether the named gateway exists and there is an HTTPRoute referencing it (by parentRef name). +func IsGatewayAndRoutePresent(ctx context.Context, client dynamic.Interface, gatewayName, routeName string) (bool, error) { + gatewayGVR := schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource} + httpRouteGVR := schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: httpRouteResource} + + _, err := client.Resource(gatewayGVR).Namespace(namespace).Get(ctx, gatewayName, k8sv1.GetOptions{}) + if err != nil { + return false, fmt.Errorf("gateway %s not found: %w", gatewayName, err) + } + + list, err := client.Resource(httpRouteGVR).Namespace(namespace).List(ctx, k8sv1.ListOptions{}) + if err != nil { + return false, fmt.Errorf("failed to list HTTPRoutes: %w", err) + } + + for _, item := range list.Items { + if routeName != "" && item.GetName() != routeName { + continue + } + spec, found, err := unstructured.NestedSlice(item.Object, "spec", "parentRefs") + if err != nil || !found { + continue + } + for _, p := range spec { + m, ok := p.(map[string]interface{}) + if !ok { + continue + } + if name, ok := m["name"].(string); ok && name == gatewayName { + if ns, ok := m["namespace"].(string); ok { + if ns == namespace { + return true, nil + } + } else { + return true, nil + } + } + } + } + + return false, nil +} diff --git a/cr-synchronizer/getters/gateway_service_generator_test.go b/cr-synchronizer/getters/gateway_service_generator_test.go new file mode 100644 index 0000000..392da4d --- /dev/null +++ b/cr-synchronizer/getters/gateway_service_generator_test.go @@ -0,0 +1,190 @@ +package getters + +import ( + "context" + "os" + "testing" + + k8sv1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes/scheme" +) + +func TestServiceAppliedWhenGatewayAndRouteExist(t *testing.T) { + ns := "test-ns" + namespace = ns + ctx := context.Background() + os.Setenv("ISTIO_INTERGATION", "true") + defer os.Unsetenv("ISTIO_INTERGATION") + + client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme.Scheme, map[schema.GroupVersionResource]string{ + {Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource}: "GatewayList", + {Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: httpRouteResource}: "HTTPRouteList", + {Group: "", Version: "v1", Resource: "services"}: "ServiceList", + }) + + gw := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]interface{}{ + "name": "istio-gateway", + }, + }} + + if _, err := client.Resource(schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource}).Namespace(ns).Create(ctx, gw, k8sv1.CreateOptions{}); err != nil { + t.Fatalf("failed to create gateway: %v", err) + } + + route := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "HTTPRoute", + "metadata": map[string]interface{}{ + "name": "fb-route", + }, + "spec": map[string]interface{}{ + "parentRefs": []interface{}{map[string]interface{}{"name": "istio-gateway"}}, + }, + }} + + if _, err := client.Resource(schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: httpRouteResource}).Namespace(ns).Create(ctx, route, k8sv1.CreateOptions{}); err != nil { + t.Fatalf("failed to create httproute: %v", err) + } + + svc := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "test-svc", + "annotations": map[string]interface{}{ + "gateway.target": "istio-gateway", + "gateway.route": "fb-route", + }, + }, + "spec": map[string]interface{}{ + "ports": []interface{}{map[string]interface{}{"protocol": "TCP", "port": int64(8080), "targetPort": int64(8080)}}, + }, + }} + + g := NewGatewayServiceGenerator(ctx, []unstructured.Unstructured{svc}, client, 10) + g.Generate() + + svcGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + if _, err := client.Resource(svcGVR).Namespace(ns).Get(ctx, "test-svc", k8sv1.GetOptions{}); err != nil { + t.Fatalf("expected service to be created, got error: %v", err) + } +} + +func TestServiceNotAppliedWhenEnvDisabled(t *testing.T) { + ns := "test-ns" + namespace = ns + ctx := context.Background() + os.Setenv("ISTIO_INTERGATION", "false") + defer os.Unsetenv("ISTIO_INTERGATION") + + client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme.Scheme, map[schema.GroupVersionResource]string{ + {Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource}: "GatewayList", + {Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: httpRouteResource}: "HTTPRouteList", + {Group: "", Version: "v1", Resource: "services"}: "ServiceList", + }) + + gw := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]interface{}{ + "name": "istio-gateway", + }, + }} + + if _, err := client.Resource(schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource}).Namespace(ns).Create(ctx, gw, k8sv1.CreateOptions{}); err != nil { + t.Fatalf("failed to create gateway: %v", err) + } + + route := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "HTTPRoute", + "metadata": map[string]interface{}{ + "name": "fb-route", + }, + "spec": map[string]interface{}{ + "parentRefs": []interface{}{map[string]interface{}{"name": "istio-gateway"}}, + }, + }} + + if _, err := client.Resource(schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: httpRouteResource}).Namespace(ns).Create(ctx, route, k8sv1.CreateOptions{}); err != nil { + t.Fatalf("failed to create httproute: %v", err) + } + + svc := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "test-svc", + "annotations": map[string]interface{}{ + "gateway.target": "istio-gateway", + "gateway.route": "fb-route", + }, + }, + "spec": map[string]interface{}{ + "ports": []interface{}{map[string]interface{}{"protocol": "TCP", "port": int64(8080), "targetPort": int64(8080)}}, + }, + }} + + g := NewGatewayServiceGenerator(ctx, []unstructured.Unstructured{svc}, client, 10) + g.Generate() + + svcGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + if _, err := client.Resource(svcGVR).Namespace(ns).Get(ctx, "test-svc", k8sv1.GetOptions{}); err == nil { + t.Fatalf("expected service NOT to be created since ISTIO_INTERGATION is false") + } +} + +func TestServiceNotAppliedWhenRouteMissing(t *testing.T) { + ns := "test-ns" + namespace = ns + ctx := context.Background() + os.Setenv("ISTIO_INTERGATION", "true") + defer os.Unsetenv("ISTIO_INTERGATION") + + client := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme.Scheme, map[schema.GroupVersionResource]string{ + {Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource}: "GatewayList", + {Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: httpRouteResource}: "HTTPRouteList", + {Group: "", Version: "v1", Resource: "services"}: "ServiceList", + }) + + gw := &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]interface{}{ + "name": "istio-gateway", + }, + }} + + if _, err := client.Resource(schema.GroupVersionResource{Group: gatewayGVRGroup, Version: gatewayGVRVersion, Resource: gatewayGVRResource}).Namespace(ns).Create(ctx, gw, k8sv1.CreateOptions{}); err != nil { + t.Fatalf("failed to create gateway: %v", err) + } + + svc := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "test-svc", + "annotations": map[string]interface{}{ + "gateway.target": "istio-gateway", + "gateway.route": "fb-route", + }, + }, + "spec": map[string]interface{}{ + "ports": []interface{}{map[string]interface{}{"protocol": "TCP", "port": int64(8080), "targetPort": int64(8080)}}, + }, + }} + + g := NewGatewayServiceGenerator(ctx, []unstructured.Unstructured{svc}, client, 10) + g.Generate() + + svcGVR := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"} + if _, err := client.Resource(svcGVR).Namespace(ns).Get(ctx, "test-svc", k8sv1.GetOptions{}); err == nil { + t.Fatalf("expected service NOT to be created since route is missing") + } +}