Skip to content
Open
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
6 changes: 5 additions & 1 deletion core-istio-mesh/charts/istio-components/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
type: application
dependencies:
- name: coretpl
version: "0.2.3"
repository: "https://netcracker.github.io/qubership-core-bootstrap/"
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ include "coretpl.synchronizer.hooks" . }}
Original file line number Diff line number Diff line change
@@ -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 }}'
Expand All @@ -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 }}'
Expand Down
8 changes: 7 additions & 1 deletion core-istio-mesh/charts/istio-components/values.yaml
Original file line number Diff line number Diff line change
@@ -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
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"
2 changes: 2 additions & 0 deletions coretpl/templates/_synchronizer.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/*") }}
Expand Down
92 changes: 92 additions & 0 deletions cr-synchronizer/README.md
Original file line number Diff line number Diff line change
@@ -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
```
7 changes: 7 additions & 0 deletions cr-synchronizer/getters/deploymentGenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -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
}

Expand Down
149 changes: 149 additions & 0 deletions cr-synchronizer/getters/gateway_service_generator.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading