From aaccf8ba00b1201358c90085f1d98975c0a185df Mon Sep 17 00:00:00 2001 From: meijun Date: Mon, 19 Jan 2026 18:41:06 +0800 Subject: [PATCH 1/2] fix service env issue and support specify service-name --- aenv/src/aenv/client/scheduler_client.py | 6 ++++- aenv/src/aenv/core/models.py | 4 +++ aenv/src/cli/cmds/service.py | 27 +++++++++++++++++++ api-service/controller/env_instance.go | 2 +- api-service/controller/env_service.go | 7 ++++- .../aenvhub_http_server/aenv_pod_handler.go | 4 +-- .../aenv_service_handler.go | 16 +++++++++-- docs/guide/cli.md | 15 +++++++++++ 8 files changed, 74 insertions(+), 7 deletions(-) diff --git a/aenv/src/aenv/client/scheduler_client.py b/aenv/src/aenv/client/scheduler_client.py index 72ed288..d3652d5 100644 --- a/aenv/src/aenv/client/scheduler_client.py +++ b/aenv/src/aenv/client/scheduler_client.py @@ -357,6 +357,7 @@ async def wait_for_status( async def create_env_service( self, name: str, + service_name: Optional[str] = None, replicas: int = 1, environment_variables: Optional[Dict[str, str]] = None, owner: Optional[str] = None, @@ -379,6 +380,8 @@ async def create_env_service( Args: name: Service name (envName format: name@version) + service_name: Optional custom service name. If not specified, will be auto-generated as "{envName}-svc-{random}". + Must follow Kubernetes DNS naming conventions. replicas: Number of replicas (default: 1, must be 1 if storage_size is specified) environment_variables: Optional environment variables owner: Optional owner of the service @@ -407,10 +410,11 @@ async def create_env_service( from aenv.core.models import EnvServiceCreateRequest logger.info( - f"Creating environment service: {name}, replicas: {replicas}, owner: {owner}" + f"Creating environment service: {name}, service_name: {service_name}, replicas: {replicas}, owner: {owner}" ) request = EnvServiceCreateRequest( envName=name, + service_name=service_name, replicas=replicas, environment_variables=environment_variables, owner=owner, diff --git a/aenv/src/aenv/core/models.py b/aenv/src/aenv/core/models.py index af645ac..93d4a4b 100644 --- a/aenv/src/aenv/core/models.py +++ b/aenv/src/aenv/core/models.py @@ -124,6 +124,10 @@ class EnvServiceCreateRequest(BaseModel): """Request to create an environment service.""" envName: str = Field(description="Environment name") + service_name: Optional[str] = Field( + None, + description="Custom service name. If not specified, will be auto-generated as '{envName}-svc-{random}'. Must follow Kubernetes DNS naming conventions.", + ) replicas: int = Field(default=1, description="Number of replicas") environment_variables: Optional[Dict[str, str]] = Field( None, description="Environment variables" diff --git a/aenv/src/cli/cmds/service.py b/aenv/src/cli/cmds/service.py index 58d69e7..d84cc57 100644 --- a/aenv/src/cli/cmds/service.py +++ b/aenv/src/cli/cmds/service.py @@ -97,6 +97,12 @@ def service(cfg: Config): @service.command("create") @click.argument("env_name", required=False) +@click.option( + "--service-name", + "-s", + type=str, + help="Custom service name (default: auto-generated as envName-svc-random). Must follow Kubernetes DNS naming conventions (lowercase alphanumeric and hyphens).", +) @click.option( "--replicas", "-r", @@ -133,6 +139,7 @@ def service(cfg: Config): def create( cfg: Config, env_name: Optional[str], + service_name: Optional[str], replicas: Optional[int], port: Optional[int], environment_variables: tuple, @@ -217,6 +224,21 @@ def create( ) raise click.Abort() + # Validate service_name if provided (must follow Kubernetes DNS naming conventions) + if service_name: + import re + # Kubernetes DNS-1123 subdomain: lowercase alphanumeric, hyphens, dots; max 253 chars; must start/end with alphanumeric + dns_pattern = r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + if not re.match(dns_pattern, service_name) or len(service_name) > 253: + console.print( + "[red]Error:[/red] Invalid service name. Service name must:\n" + " - Use only lowercase letters, numbers, hyphens, and dots\n" + " - Start and end with an alphanumeric character\n" + " - Be no longer than 253 characters\n" + f" - Example: 'my-service', 'app-v1', 'web-frontend-prod'" + ) + raise click.Abort() + # Merge parameters: CLI > config.json > defaults final_replicas = ( replicas if replicas is not None else service_config.get("replicas", 1) @@ -297,6 +319,10 @@ def create( # Display configuration summary console.print(f"[cyan]🚀 Creating environment service:[/cyan] {env_name}") + if service_name: + console.print(f" Service Name: {service_name} (custom)") + else: + console.print(f" Service Name: auto-generated") console.print(f" Replicas: {final_replicas}") if final_port: console.print(f" Port: {final_port}") @@ -331,6 +357,7 @@ async def _create(): ) as client: return await client.create_env_service( name=env_name, + service_name=service_name, replicas=final_replicas, environment_variables=env_vars, owner=owner, diff --git a/api-service/controller/env_instance.go b/api-service/controller/env_instance.go index b76a785..8e23a00 100644 --- a/api-service/controller/env_instance.go +++ b/api-service/controller/env_instance.go @@ -96,7 +96,7 @@ func (ctrl *EnvInstanceController) CreateEnvInstance(c *gin.Context) { backendEnv.DeployConfig["secondImageName"] = secondImageName } if req.EnvironmentVariables != nil { - backendEnv.DeployConfig["environmentVariables"] = req.EnvironmentVariables + backendEnv.DeployConfig["environment_variables"] = req.EnvironmentVariables } if req.Arguments != nil { backendEnv.DeployConfig["arguments"] = req.Arguments diff --git a/api-service/controller/env_service.go b/api-service/controller/env_service.go index 517f214..56ab586 100644 --- a/api-service/controller/env_service.go +++ b/api-service/controller/env_service.go @@ -53,6 +53,7 @@ func NewEnvServiceController( // CreateEnvServiceRequest represents the request body for creating an EnvService type CreateEnvServiceRequest struct { EnvName string `json:"envName" binding:"required"` + ServiceName string `json:"service_name"` // Optional custom service name Replicas int32 `json:"replicas"` EnvironmentVariables map[string]string `json:"environment_variables"` Owner string `json:"owner"` @@ -110,12 +111,16 @@ func (ctrl *EnvServiceController) CreateEnvService(c *gin.Context) { backendEnv.DeployConfig = make(map[string]interface{}) } if req.EnvironmentVariables != nil { - backendEnv.DeployConfig["environmentVariables"] = req.EnvironmentVariables + backendEnv.DeployConfig["environment_variables"] = req.EnvironmentVariables } backendEnv.DeployConfig["replicas"] = req.Replicas if req.Owner != "" { backendEnv.DeployConfig["owner"] = req.Owner } + // Pass custom serviceName to DeployConfig if provided + if req.ServiceName != "" { + backendEnv.DeployConfig["serviceName"] = req.ServiceName + } // Storage configuration if req.PVCName != "" { diff --git a/controller/pkg/aenvhub_http_server/aenv_pod_handler.go b/controller/pkg/aenvhub_http_server/aenv_pod_handler.go index dab3cee..eff8cef 100644 --- a/controller/pkg/aenvhub_http_server/aenv_pod_handler.go +++ b/controller/pkg/aenvhub_http_server/aenv_pod_handler.go @@ -505,9 +505,9 @@ func MergePodImage(pod *corev1.Pod, aenv *model.AEnvHubEnv) { func applyConfig(configs map[string]interface{}, container *corev1.Container) { - klog.Infof("config environment variables: %v", configs["environmentVariables"]) + klog.Infof("config environment variables: %v", configs["environment_variables"]) - if environmentVariables, ok := configs["environmentVariables"].(map[string]interface{}); ok { + if environmentVariables, ok := configs["environment_variables"].(map[string]interface{}); ok { if len(environmentVariables) > 0 { for k, v := range environmentVariables { container.Env = append(container.Env, corev1.EnvVar{ diff --git a/controller/pkg/aenvhub_http_server/aenv_service_handler.go b/controller/pkg/aenvhub_http_server/aenv_service_handler.go index ea22d72..0843dd4 100644 --- a/controller/pkg/aenvhub_http_server/aenv_service_handler.go +++ b/controller/pkg/aenvhub_http_server/aenv_service_handler.go @@ -166,8 +166,20 @@ func (h *AEnvServiceHandler) createService(w http.ResponseWriter, r *http.Reques ctx := r.Context() - // Generate service name - serviceName := fmt.Sprintf("%s-svc-%s", aenvHubEnv.Name, RandString(6)) + // Generate service name: use custom serviceName from DeployConfig if provided, otherwise auto-generate + var serviceName string + if customServiceName, ok := aenvHubEnv.DeployConfig["serviceName"]; ok { + if customServiceNameStr, ok := customServiceName.(string); ok && customServiceNameStr != "" { + serviceName = customServiceNameStr + klog.Infof("Using custom service name: %s", serviceName) + } + } + + // If no custom serviceName provided, auto-generate using envName and random suffix + if serviceName == "" { + serviceName = fmt.Sprintf("%s-svc-%s", aenvHubEnv.Name, RandString(6)) + klog.Infof("Auto-generated service name: %s", serviceName) + } // Get PVC name from deploy config, default to envName pvcName := aenvHubEnv.Name // Default PVC name equals envName diff --git a/docs/guide/cli.md b/docs/guide/cli.md index cef7e83..a88df6a 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -597,6 +597,9 @@ aenv service create # Create with explicit environment name aenv service create myapp@1.0.0 +# Create with custom service name +aenv service create myapp@1.0.0 --service-name my-custom-service + # Create with 3 replicas and custom port (no storage) aenv service create myapp@1.0.0 --replicas 3 --port 8000 @@ -611,6 +614,7 @@ aenv service create myapp@1.0.0 -e DB_HOST=postgres -e CACHE_SIZE=1024 | Option | Short | Description | Default | |---|---|---|---| +| `--service-name` | `-s` | Custom service name (must follow Kubernetes DNS naming conventions) | auto-generated as `{envName}-svc-{random}` | | `--replicas` | `-r` | Number of replicas | 1 or from config | | `--port` | `-p` | Service port | 8080 or from config | | `--env` | `-e` | Environment variables (KEY=VALUE) | - | @@ -637,6 +641,17 @@ Storage settings are read from `config.json`'s `deployConfig.service`: - When storage is enabled, replicas must be 1 (enforced by backend) - Services run indefinitely without TTL - Services get cluster DNS service URLs for internal access +- **Service Name**: Custom service names must follow Kubernetes DNS naming conventions: + - Use only lowercase letters, numbers, hyphens, and dots + - Start and end with an alphanumeric character + - Be no longer than 253 characters + - Example: `my-service`, `app-v1`, `web-frontend-prod` + - If not specified, auto-generated as `{envName}-svc-{random}` (e.g., `myapp-svc-abc123`) +- The service name becomes: + - Kubernetes Deployment name + - Kubernetes Service name + - Service URL prefix: `{serviceName}.{namespace}.{domain}:{port}` + - Unique identifier for all operations (get, update, delete) #### `service list` - List Services From db550fc7aac26cdaf80a3061fc61d141fc1f9678 Mon Sep 17 00:00:00 2001 From: meijun Date: Mon, 19 Jan 2026 19:57:20 +0800 Subject: [PATCH 2/2] fix lint --- aenv/src/cli/cmds/service.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aenv/src/cli/cmds/service.py b/aenv/src/cli/cmds/service.py index d84cc57..3a507c3 100644 --- a/aenv/src/cli/cmds/service.py +++ b/aenv/src/cli/cmds/service.py @@ -227,15 +227,18 @@ def create( # Validate service_name if provided (must follow Kubernetes DNS naming conventions) if service_name: import re + # Kubernetes DNS-1123 subdomain: lowercase alphanumeric, hyphens, dots; max 253 chars; must start/end with alphanumeric - dns_pattern = r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$' + dns_pattern = ( + r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$" + ) if not re.match(dns_pattern, service_name) or len(service_name) > 253: console.print( "[red]Error:[/red] Invalid service name. Service name must:\n" " - Use only lowercase letters, numbers, hyphens, and dots\n" " - Start and end with an alphanumeric character\n" " - Be no longer than 253 characters\n" - f" - Example: 'my-service', 'app-v1', 'web-frontend-prod'" + " - Example: 'my-service', 'app-v1', 'web-frontend-prod'" ) raise click.Abort() @@ -322,7 +325,7 @@ def create( if service_name: console.print(f" Service Name: {service_name} (custom)") else: - console.print(f" Service Name: auto-generated") + console.print(" Service Name: auto-generated") console.print(f" Replicas: {final_replicas}") if final_port: console.print(f" Port: {final_port}")