Skip to content
Merged
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 aenv/src/aenv/client/scheduler_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions aenv/src/aenv/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions aenv/src/cli/cmds/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -217,6 +224,24 @@ 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"
" - 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)
Expand Down Expand Up @@ -297,6 +322,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(" Service Name: auto-generated")
console.print(f" Replicas: {final_replicas}")
if final_port:
console.print(f" Port: {final_port}")
Expand Down Expand Up @@ -331,6 +360,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,
Expand Down
2 changes: 1 addition & 1 deletion api-service/controller/env_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion api-service/controller/env_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
Comment on lines +121 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The key serviceName (camelCase) is being used to store the custom service name in DeployConfig. However, this PR is also fixing other keys in DeployConfig to use snake_case (e.g., environment_variables). For consistency across the codebase, please use service_name (snake_case) as the key.

Suggested change
if req.ServiceName != "" {
backendEnv.DeployConfig["serviceName"] = req.ServiceName
}
if req.ServiceName != "" {
backendEnv.DeployConfig["service_name"] = req.ServiceName
}


// Storage configuration
if req.PVCName != "" {
Expand Down
4 changes: 2 additions & 2 deletions controller/pkg/aenvhub_http_server/aenv_pod_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
16 changes: 14 additions & 2 deletions controller/pkg/aenvhub_http_server/aenv_service_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment on lines +169 to +182
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for determining the service name can be improved in two ways:

  1. Consistency: The key serviceName should be service_name to be consistent with other DeployConfig keys (like environment_variables) and the API request model. I've made a corresponding suggestion in api-service/controller/env_service.go.
  2. Readability: The logic can be refactored into a more concise if/else structure to improve readability.
Suggested change
// 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)
}
// Generate service name: use custom serviceName from DeployConfig if provided, otherwise auto-generate
var serviceName string
if customName, ok := aenvHubEnv.DeployConfig["service_name"].(string); ok && customName != "" {
serviceName = customName
klog.Infof("Using custom service name: %s", serviceName)
} else {
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
Expand Down
15 changes: 15 additions & 0 deletions docs/guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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) | - |
Expand All @@ -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`)
Comment on lines +644 to +649
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This documentation for service name constraints is incorrect and reflects the faulty validation logic in the CLI. A Kubernetes service name must be a DNS-1123 label, which does not allow dots and has a maximum length of 63 characters. The documentation should be updated to reflect the correct constraints.

Suggested change
- **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`)
- **Service Name**: Custom service names must follow Kubernetes DNS-1123 label standards:
- Must be no longer than 63 characters
- Must use only lowercase letters, numbers, and hyphens ('-')
- Must start and end with an alphanumeric character
- 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

Expand Down
Loading