Unverified Commit 1e8b2866 authored by hhzhang16's avatar hhzhang16 Committed by GitHub
Browse files

feat: add ingress to graph deployments (#960)

parent a590d103
......@@ -36,6 +36,7 @@ export INGRESS_ENABLED="${INGRESS_ENABLED:=false}"
export ISTIO_ENABLED="${ISTIO_ENABLED:=false}"
export ISTIO_GATEWAY="${ISTIO_GATEWAY:=istio-system/istio-ingressgateway}"
export INGRESS_CLASS="${INGRESS_CLASS:=nginx}"
export VIRTUAL_SERVICE_SUPPORTS_HTTPS="${VIRTUAL_SERVICE_SUPPORTS_HTTPS:=false}"
# Add command line options
INTERACTIVE=false
......@@ -140,8 +141,9 @@ echo "ISTIO_ENABLED: $ISTIO_ENABLED"
echo "INGRESS_CLASS: $INGRESS_CLASS"
echo "ISTIO_GATEWAY: $ISTIO_GATEWAY"
echo "DYNAMO_INGRESS_SUFFIX: $DYNAMO_INGRESS_SUFFIX"
echo "VIRTUAL_SERVICE_SUPPORTS_HTTPS: $VIRTUAL_SERVICE_SUPPORTS_HTTPS"
envsubst '${NAMESPACE} ${RELEASE_NAME} ${DOCKER_USERNAME} ${DOCKER_PASSWORD} ${DOCKER_SERVER} ${IMAGE_TAG} ${DYNAMO_INGRESS_SUFFIX} ${PIPELINES_DOCKER_SERVER} ${PIPELINES_DOCKER_USERNAME} ${PIPELINES_DOCKER_PASSWORD} ${DOCKER_SECRET_NAME} ${INGRESS_ENABLED} ${ISTIO_ENABLED} ${INGRESS_CLASS} ${ISTIO_GATEWAY}' < dynamo-platform-values.yaml > generated-values.yaml
envsubst '${NAMESPACE} ${RELEASE_NAME} ${DOCKER_USERNAME} ${DOCKER_PASSWORD} ${DOCKER_SERVER} ${IMAGE_TAG} ${DYNAMO_INGRESS_SUFFIX} ${PIPELINES_DOCKER_SERVER} ${PIPELINES_DOCKER_USERNAME} ${PIPELINES_DOCKER_PASSWORD} ${DOCKER_SECRET_NAME} ${INGRESS_ENABLED} ${ISTIO_ENABLED} ${INGRESS_CLASS} ${ISTIO_GATEWAY} ${VIRTUAL_SERVICE_SUPPORTS_HTTPS}' < dynamo-platform-values.yaml > generated-values.yaml
echo "generated file contents:"
cat generated-values.yaml
......
......@@ -40,6 +40,7 @@ dynamo-operator:
username: ${PIPELINES_DOCKER_USERNAME}
password: ${PIPELINES_DOCKER_PASSWORD}
imageBuildEngine: buildkit
virtualServiceSupportsHTTPS: ${VIRTUAL_SERVICE_SUPPORTS_HTTPS}
dynamo-api-store:
namespaceRestriction:
......
......@@ -253,6 +253,14 @@ if [ "$CONFIG_TYPE" = "istio" ]; then
fi
fi
# Ask if the Istio gateway supports HTTPS
read -p "Does your Istio gateway support HTTPS? (y/n): " SUPPORTS_HTTPS_REPLY
if [[ "$SUPPORTS_HTTPS_REPLY" =~ ^[Yy]$ ]]; then
export VIRTUAL_SERVICE_SUPPORTS_HTTPS=true
else
export VIRTUAL_SERVICE_SUPPORTS_HTTPS=false
fi
elif [ "$CONFIG_TYPE" = "ingress" ]; then
echo -e "${CYAN}Configuring Ingress settings...${NC}"
......
......@@ -86,6 +86,9 @@ spec:
{{- if .Values.dynamo.ingressHostSuffix }}
- --ingress-host-suffix={{ .Values.dynamo.ingressHostSuffix }}
{{- end }}
{{- if .Values.dynamo.virtualServiceSupportsHTTPS }}
- --virtual-service-supports-https={{ .Values.dynamo.virtualServiceSupportsHTTPS }}
{{- end }}
command:
- /manager
......
......@@ -71,6 +71,7 @@ func main() {
var natsAddr string
var etcdAddr string
var istioVirtualServiceGateway string
var virtualServiceSupportsHTTPS bool
var ingressControllerClassName string
var ingressControllerTLSSecretName string
var ingressHostSuffix string
......@@ -91,6 +92,8 @@ func main() {
flag.StringVar(&etcdAddr, "etcdAddr", "", "address of the etcd server")
flag.StringVar(&istioVirtualServiceGateway, "istio-virtual-service-gateway", "",
"The name of the istio virtual service gateway to use")
flag.BoolVar(&virtualServiceSupportsHTTPS, "virtual-service-supports-https", false,
"If set, assume VirtualService endpoints are HTTPS")
flag.StringVar(&ingressControllerClassName, "ingress-controller-class-name", "",
"The name of the ingress controller class to use")
flag.StringVar(&ingressControllerTLSSecretName, "ingress-controller-tls-secret-name", "",
......@@ -107,6 +110,7 @@ func main() {
ctrlConfig := commonController.Config{
RestrictedNamespace: restrictedNamespace,
VirtualServiceSupportsHTTPS: virtualServiceSupportsHTTPS,
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
......
......@@ -24,7 +24,7 @@ const (
// nolint: gosec
EnvApiStoreApiToken = "API_STORE_API_TOKEN"
EnvDynamoServicePort = "PORT"
EnvDynamoServicePort = "DYNAMO_PORT"
EnvDockerRegistryServer = "DOCKER_REGISTRY_SERVER"
EnvDockerRegistrySecret = "DOCKER_REGISTRY_SECRET_NAME"
......
......@@ -740,7 +740,7 @@ func (r *DynamoComponentDeploymentReconciler) generateIngress(ctx context.Contex
Service: &networkingv1.IngressServiceBackend{
Name: opt.dynamoComponentDeployment.Name,
Port: networkingv1.ServiceBackendPort{
Number: 3000,
Number: commonconsts.DynamoServicePort,
},
},
},
......@@ -800,7 +800,7 @@ func (r *DynamoComponentDeploymentReconciler) generateVirtualService(ctx context
Destination: &istioNetworking.Destination{
Host: opt.dynamoComponentDeployment.Name,
Port: &istioNetworking.PortSelector{
Number: 3000,
Number: commonconsts.DynamoServicePort,
},
},
},
......@@ -1121,7 +1121,7 @@ func (r *DynamoComponentDeploymentReconciler) generatePodTemplateSpec(ctx contex
// todo : remove this line when https://github.com/ai-dynamo/dynamo/issues/345 is fixed
enableDependsOption := false
if len(opt.dynamoComponentDeployment.Spec.ExternalServices) > 0 && enableDependsOption {
serviceSuffix := fmt.Sprintf("%s.svc.cluster.local:3000", opt.dynamoComponentDeployment.Namespace)
serviceSuffix := fmt.Sprintf("%s.svc.cluster.local:%d", opt.dynamoComponentDeployment.Namespace, containerPort)
keys := make([]string, 0, len(opt.dynamoComponentDeployment.Spec.ExternalServices))
for key := range opt.dynamoComponentDeployment.Spec.ExternalServices {
......
......@@ -143,7 +143,7 @@ func (r *DynamoGraphDeploymentReconciler) Reconcile(ctx context.Context, req ctr
}
}
if deployment.Spec.Ingress.Enabled {
dynamoDeployment.SetEndpointStatus((r.isEndpointSecured()), getIngressHost(deployment.Spec.Ingress))
dynamoDeployment.SetEndpointStatus(r.isEndpointSecured(), getIngressHost(deployment.Spec.Ingress))
}
}
......@@ -238,6 +238,9 @@ func (r *DynamoGraphDeploymentReconciler) generateDefaultIngressSpec(dynamoDeplo
}
func (r *DynamoGraphDeploymentReconciler) isEndpointSecured() bool {
if r.VirtualServiceGateway != "" && r.Config.VirtualServiceSupportsHTTPS {
return true
}
return r.IngressControllerTLSSecret != ""
}
......
......@@ -30,6 +30,8 @@ import (
type Config struct {
// Enable resources filtering, only the resources belonging to the given namespace will be handled.
RestrictedNamespace string
// If true, assume VirtualService endpoints are HTTPS
VirtualServiceSupportsHTTPS bool
}
func EphemeralDeploymentEventFilter(config Config) predicate.Predicate {
......
......@@ -70,6 +70,8 @@ type Config struct {
Resources *Resources `yaml:"resources,omitempty"`
Traffic *Traffic `yaml:"traffic,omitempty"`
Autoscaling *Autoscaling `yaml:"autoscaling,omitempty"`
HttpExposed bool `yaml:"http_exposed,omitempty"`
ApiEndpoints []string `yaml:"api_endpoints,omitempty"`
}
type ServiceConfig struct {
......@@ -250,13 +252,13 @@ func GenerateDynamoComponentsDeployments(ctx context.Context, parentDynamoGraphD
deployment.Spec.DynamoNamespace = &dynamoNamespace
dynamoServices[service.Name] = fmt.Sprintf("%s/%s", service.Config.Dynamo.Name, dynamoNamespace)
labels[commonconsts.KubeLabelDynamoNamespace] = dynamoNamespace
} else {
// dynamo is not enabled
if config.EntryService == service.Name {
// enable virtual service for the entry service
deployment.Spec.Ingress = *ingressSpec
}
// Check http_exposed independently
if config.EntryService == service.Name && service.Config.HttpExposed {
deployment.Spec.Ingress = *ingressSpec
// TODO (maybe): add paths to IngressSpec
}
if service.Config.Resources != nil {
deployment.Spec.Resources = &compounaiCommon.Resources{
Requests: &compounaiCommon.ResourceItem{
......
......@@ -182,6 +182,7 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
Name: "service1",
Dependencies: []map[string]string{{"service": "service2"}},
Config: Config{
HttpExposed: true,
Resources: &Resources{
CPU: "1",
Memory: "1Gi",
......@@ -260,6 +261,10 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
},
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: nil,
PodSelector: nil,
},
},
"service2": {
ObjectMeta: metav1.ObjectMeta{
......@@ -283,6 +288,18 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
commonconsts.KubeLabelDynamoComponent: "service2",
commonconsts.KubeLabelDynamoNamespace: "default",
},
Ingress: v1alpha1.IngressSpec{
Enabled: false,
Host: "",
UseVirtualService: false,
VirtualServiceGateway: nil,
HostPrefix: nil,
Annotations: nil,
Labels: nil,
TLS: nil,
HostSuffix: nil,
IngressControllerClassName: nil,
},
},
},
},
......@@ -309,6 +326,7 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
Name: "service1",
Dependencies: []map[string]string{{"service": "service2"}},
Config: Config{
HttpExposed: true,
Resources: &Resources{
CPU: "1",
Memory: "1Gi",
......@@ -374,12 +392,27 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
DeploymentSelectorValue: "service2/dynamo-test-dynamographdeployment",
},
},
Ingress: v1alpha1.IngressSpec{},
Ingress: v1alpha1.IngressSpec{
Enabled: false,
Host: "",
UseVirtualService: false,
VirtualServiceGateway: nil,
HostPrefix: nil,
Annotations: nil,
Labels: nil,
TLS: nil,
HostSuffix: nil,
IngressControllerClassName: nil,
},
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: nil,
PodSelector: nil,
},
},
"service2": {
ObjectMeta: metav1.ObjectMeta{
......@@ -403,6 +436,18 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
commonconsts.KubeLabelDynamoComponent: "service2",
commonconsts.KubeLabelDynamoNamespace: "dynamo-test-dynamographdeployment",
},
Ingress: v1alpha1.IngressSpec{
Enabled: false,
Host: "",
UseVirtualService: false,
VirtualServiceGateway: nil,
HostPrefix: nil,
Annotations: nil,
Labels: nil,
TLS: nil,
HostSuffix: nil,
IngressControllerClassName: nil,
},
},
},
},
......@@ -462,6 +507,250 @@ func TestGenerateDynamoComponentsDeployments(t *testing.T) {
},
wantErr: true,
},
{
name: "Test GenerateDynamoComponentsDeployments ingress enabled by default",
args: args{
parentDynamoGraphDeployment: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dynamographdeployment",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
DynamoGraph: "dynamocomponent:ac4e234",
},
},
config: &DynamoGraphConfig{
DynamoTag: "dynamocomponent:MyServiceIngressEnabled",
EntryService: "service1",
Services: []ServiceConfig{
{
Name: "service1",
Config: Config{
HttpExposed: true,
},
},
},
},
ingressSpec: &v1alpha1.IngressSpec{
Enabled: true,
Host: "test-dynamographdeployment",
},
},
want: map[string]*v1alpha1.DynamoComponentDeployment{
"service1": {
ObjectMeta: metav1.ObjectMeta{
Name: "test-dynamographdeployment-service1",
Namespace: "default",
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
DynamoComponent: "dynamocomponent:ac4e234",
DynamoTag: "dynamocomponent:MyServiceIngressEnabled",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
Annotations: nil,
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
ServiceName: "service1",
DynamoNamespace: nil,
Resources: nil,
Autoscaling: &v1alpha1.Autoscaling{
Enabled: false,
MinReplicas: 0,
MaxReplicas: 0,
Behavior: nil,
Metrics: nil,
},
Envs: nil,
EnvFromSecret: nil,
PVC: nil,
RunMode: nil,
ExternalServices: nil,
Ingress: v1alpha1.IngressSpec{
Enabled: true,
Host: "test-dynamographdeployment",
},
ExtraPodMetadata: nil,
ExtraPodSpec: nil,
LivenessProbe: nil,
ReadinessProbe: nil,
Replicas: nil,
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: nil,
PodSelector: nil,
},
},
},
wantErr: false,
},
{
name: "Test GenerateDynamoComponentsDeployments ingress explicitly disabled",
args: args{
parentDynamoGraphDeployment: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dynamographdeployment",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
DynamoGraph: "dynamocomponent:ac4e234",
},
},
config: &DynamoGraphConfig{
DynamoTag: "dynamocomponent:MyServiceIngressDisabled",
Services: []ServiceConfig{
{
Name: "service1",
Config: Config{},
},
},
},
ingressSpec: &v1alpha1.IngressSpec{
Enabled: false,
},
},
want: map[string]*v1alpha1.DynamoComponentDeployment{
"service1": {
ObjectMeta: metav1.ObjectMeta{
Name: "test-dynamographdeployment-service1",
Namespace: "default",
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
DynamoComponent: "dynamocomponent:ac4e234",
DynamoTag: "dynamocomponent:MyServiceIngressDisabled",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
Annotations: nil,
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
ServiceName: "service1",
DynamoNamespace: nil,
Resources: nil,
Autoscaling: &v1alpha1.Autoscaling{
Enabled: false,
MinReplicas: 0,
MaxReplicas: 0,
Behavior: nil,
Metrics: nil,
},
Envs: nil,
EnvFromSecret: nil,
PVC: nil,
RunMode: nil,
ExternalServices: nil,
Ingress: v1alpha1.IngressSpec{
Enabled: false,
Host: "",
UseVirtualService: false,
VirtualServiceGateway: nil,
HostPrefix: nil,
Annotations: nil,
Labels: nil,
TLS: nil,
HostSuffix: nil,
IngressControllerClassName: nil,
},
ExtraPodMetadata: nil,
ExtraPodSpec: nil,
LivenessProbe: nil,
ReadinessProbe: nil,
Replicas: nil,
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: nil,
PodSelector: nil,
},
},
},
wantErr: false,
},
{
name: "Test GenerateDynamoComponentsDeployments ingress custom host",
args: args{
parentDynamoGraphDeployment: &v1alpha1.DynamoGraphDeployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-dynamographdeployment",
Namespace: "default",
},
Spec: v1alpha1.DynamoGraphDeploymentSpec{
DynamoGraph: "dynamocomponent:ac4e234",
},
},
config: &DynamoGraphConfig{
DynamoTag: "dynamocomponent:MyServiceIngressCustomHost",
EntryService: "service1",
Services: []ServiceConfig{
{
Name: "service1",
Config: Config{
HttpExposed: true,
},
},
},
},
ingressSpec: &v1alpha1.IngressSpec{
Enabled: true,
Host: "custom-host",
},
},
want: map[string]*v1alpha1.DynamoComponentDeployment{
"service1": {
ObjectMeta: metav1.ObjectMeta{
Name: "test-dynamographdeployment-service1",
Namespace: "default",
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
},
Spec: v1alpha1.DynamoComponentDeploymentSpec{
DynamoComponent: "dynamocomponent:ac4e234",
DynamoTag: "dynamocomponent:MyServiceIngressCustomHost",
DynamoComponentDeploymentSharedSpec: v1alpha1.DynamoComponentDeploymentSharedSpec{
Annotations: nil,
Labels: map[string]string{
commonconsts.KubeLabelDynamoComponent: "service1",
},
ServiceName: "service1",
DynamoNamespace: nil,
Resources: nil,
Autoscaling: &v1alpha1.Autoscaling{
Enabled: false,
MinReplicas: 0,
MaxReplicas: 0,
Behavior: nil,
Metrics: nil,
},
Envs: nil,
EnvFromSecret: nil,
PVC: nil,
RunMode: nil,
ExternalServices: nil,
Ingress: v1alpha1.IngressSpec{
Enabled: true,
Host: "custom-host",
},
ExtraPodMetadata: nil,
ExtraPodSpec: nil,
LivenessProbe: nil,
ReadinessProbe: nil,
Replicas: nil,
},
},
Status: v1alpha1.DynamoComponentDeploymentStatus{
Conditions: nil,
PodSelector: nil,
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
......
......@@ -86,7 +86,7 @@ spec:
- name: WORKERS
value: "{{ .config.workers }}"
{{- end }}
- name: PORT
- name: DYNAMO_PORT
value: "3000"
{{- if $.Values.natsAddr }}
- name: NATS_SERVER
......
......@@ -37,6 +37,7 @@ from dynamo.runtime import DistributedRuntime, dynamo_endpoint, dynamo_worker
from dynamo.sdk import dynamo_context
from dynamo.sdk.cli.utils import append_dynamo_state
from dynamo.sdk.lib.service import LinkedServices
from dynamo.sdk.lib.utils import get_host_port
logger = logging.getLogger(__name__)
......@@ -313,16 +314,15 @@ def main(
if added_routes:
# Configure uvicorn with graceful shutdown
# get the port from PORT env var or use 8000 as default
port = int(os.environ.get("PORT", 8000))
host, port = get_host_port()
config = uvicorn.Config(
service.app, host="0.0.0.0", port=port, log_level="info"
service.app, host=host, port=port, log_level="info"
)
server = uvicorn.Server(config)
# Start the server with graceful shutdown handling
logger.info(
f"Starting FastAPI server on 0.0.0.0:{port} with routes: {added_routes}"
f"Starting FastAPI server on {config.host}:{config.port} with routes: {added_routes}"
)
server.run()
else:
......
......@@ -133,10 +133,21 @@ class DynamoService(Service[T]):
# Register Dynamo endpoints
self._dynamo_endpoints: Dict[str, DynamoEndpoint] = {}
self._api_endpoints: list[str] = []
for field in dir(inner):
value = getattr(inner, field)
if isinstance(value, DynamoEndpoint):
self._dynamo_endpoints[value.name] = value
if getattr(value, "is_api", False):
# Ensure endpoint path starts with '/'
path = (
value.name if value.name.startswith("/") else f"/{value.name}"
)
self._api_endpoints.append(path)
# If any API endpoints exist, mark service as HTTP-exposed and list endpoints
if self._api_endpoints:
self.config["http_exposed"] = True
self.config["api_endpoints"] = self._api_endpoints.copy()
self._linked_services: List[DynamoService] = [] # Track linked services
......
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
def get_host_port():
"""Gets host and port from environment variables. Defaults to 0.0.0.0:8000."""
port = int(os.environ.get("DYNAMO_PORT", 8000))
host = os.environ.get("DYNAMO_HOST", "0.0.0.0")
return host, port
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment