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
16 changes: 16 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,22 @@ func (c *client) UpdateApp(ctx context.Context, projectID, appID string, updates
return err
}

// ValidateRuntime validates runtime configuration and returns the action to take
func (c *client) ValidateRuntime(ctx context.Context, projectID string, req *ValidateRuntimeRequest) (*ValidateRuntimeResponse, error) {
path := fmt.Sprintf("v4/projects/%s/apps/validate-runtime", projectID)
body, err := c.request(ctx, "POST", path, req, true)
if err != nil {
return nil, err
}

var response ValidateRuntimeResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}

return &response, nil
}

// CreateApp creates a new app/build for standard (cortex/custom) deployments
func (c *client) CreateApp(ctx context.Context, projectID string, payload map[string]any) (*CreateAppResponse, error) {
// Determine endpoint based on runtime type
Expand Down
1 change: 1 addition & 0 deletions internal/api/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Client interface {
FetchAppLogs(ctx context.Context, projectID, appID string, opts AppLogOptions) (*AppLogsResponse, error)

// Deploy methods
ValidateRuntime(ctx context.Context, projectID string, req *ValidateRuntimeRequest) (*ValidateRuntimeResponse, error)
CreateApp(ctx context.Context, projectID string, payload map[string]any) (*CreateAppResponse, error)
CreatePartnerApp(ctx context.Context, projectID string, payload map[string]any) (*CreateAppResponse, error)
UploadZip(ctx context.Context, uploadURL string, zipPath string) error
Expand Down
74 changes: 74 additions & 0 deletions internal/api/mock/client_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,16 @@ type BaseImageResponse struct {
Status string `json:"status"`
Digest string `json:"digest"`
}

// ValidateRuntimeRequest is the request payload for runtime validation
type ValidateRuntimeRequest struct {
Runtime string `json:"runtime"`
Params map[string]any `json:"params"`
}

// ValidateRuntimeResponse is the response from runtime validation
type ValidateRuntimeResponse struct {
Valid bool `json:"valid"`
Action string `json:"action,omitempty"` // "create_app" or "create_partner_app"
Errors []string `json:"errors,omitempty"`
}
8 changes: 8 additions & 0 deletions internal/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ func runDeploy(cmd *cobra.Command, opts deployOptions, disableConfirmation bool)
return ui.NewValidationError(err)
}

// Print deprecation warnings if any
for _, warning := range projectConfig.DeprecationWarnings {
fmt.Printf("⚠️ Deprecation warning: %s\n", warning)
}
if len(projectConfig.DeprecationWarnings) > 0 {
fmt.Println() // Add spacing after warnings
}

// Validate project config
if err := projectconfig.Validate(projectConfig); err != nil {
return ui.NewValidationError(fmt.Errorf("invalid configuration: %w", err))
Expand Down
10 changes: 6 additions & 4 deletions internal/commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,18 @@ func runInit(cmd *cobra.Command, name string, dir string) error {

// createDefaultConfig creates a cerebrium.toml file with sensible defaults
func createDefaultConfig(path string, name string) error {
// Manually construct TOML to match Python output exactly
// Using double quotes for strings and avoiding empty [cerebrium.dependencies] section
// Manually construct TOML with new runtime configuration structure
// Using double quotes for strings and avoiding empty sections
content := fmt.Sprintf(`[cerebrium.deployment]
name = "%s"
python_version = "3.11"
docker_base_image_url = "debian:bookworm-slim"
disable_auth = true
include = ['./*', 'main.py', 'cerebrium.toml']
exclude = ['.*']

[cerebrium.runtime.cortex]
python_version = "3.11"
docker_base_image_url = "debian:bookworm-slim"

[cerebrium.hardware]
cpu = 2.0
memory = 2.0
Expand Down
26 changes: 22 additions & 4 deletions internal/commands/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,18 @@ func TestRunInit(t *testing.T) {
deployment, ok := cerebrium["deployment"].(map[string]any)
require.True(t, ok, "deployment section should exist")
assert.Equal(t, projectName, deployment["name"])
assert.Equal(t, "3.11", deployment["python_version"])
assert.Equal(t, "debian:bookworm-slim", deployment["docker_base_image_url"])
assert.Equal(t, true, deployment["disable_auth"])
// python_version and docker_base_image_url are now in runtime.cortex
assert.Nil(t, deployment["python_version"], "python_version should not be in deployment")
assert.Nil(t, deployment["docker_base_image_url"], "docker_base_image_url should not be in deployment")

// Check runtime.cortex config
runtime, ok := cerebrium["runtime"].(map[string]any)
require.True(t, ok, "runtime section should exist")
cortex, ok := runtime["cortex"].(map[string]any)
require.True(t, ok, "runtime.cortex section should exist")
assert.Equal(t, "3.11", cortex["python_version"])
assert.Equal(t, "debian:bookworm-slim", cortex["docker_base_image_url"])

// Check include/exclude arrays
include, ok := deployment["include"].([]any)
Expand Down Expand Up @@ -288,10 +297,19 @@ func TestCreateDefaultConfig(t *testing.T) {
deployment, ok := cerebrium["deployment"].(map[string]any)
require.True(t, ok)
assert.Equal(t, projectName, deployment["name"])
assert.NotNil(t, deployment["python_version"])
assert.NotNil(t, deployment["docker_base_image_url"])
assert.NotNil(t, deployment["include"])
assert.NotNil(t, deployment["exclude"])
// python_version and docker_base_image_url are now in runtime.cortex
assert.Nil(t, deployment["python_version"], "python_version should not be in deployment")
assert.Nil(t, deployment["docker_base_image_url"], "docker_base_image_url should not be in deployment")

// Check runtime.cortex config
runtime, ok := cerebrium["runtime"].(map[string]any)
require.True(t, ok, "runtime section should exist")
cortex, ok := runtime["cortex"].(map[string]any)
require.True(t, ok, "runtime.cortex section should exist")
assert.NotNil(t, cortex["python_version"])
assert.NotNil(t, cortex["docker_base_image_url"])

hardware, ok := cerebrium["hardware"].(map[string]any)
require.True(t, ok)
Expand Down
5 changes: 5 additions & 0 deletions internal/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@ func loadProjectConfigOrDefault() (*projectconfig.ProjectConfig, error) {
}
}

// Print deprecation warnings if any
for _, warning := range projectConfig.DeprecationWarnings {
fmt.Printf("⚠️ Deprecation warning: %s\n", warning)
}

return projectConfig, nil
}

Expand Down
87 changes: 67 additions & 20 deletions internal/files/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,66 +13,113 @@ import (
func GenerateDependencyFiles(config *projectconfig.ProjectConfig) (map[string]string, error) {
files := make(map[string]string)

// Get effective dependencies (merged from top-level and runtime-specific)
effectiveDeps := config.GetEffectiveDependencies()

// Generate pip requirements
if err := generateDependencyFile(files, "requirements.txt", config.Dependencies.Pip, config.Dependencies.Paths.Pip); err != nil {
// Pass deprecated Paths.Pip as fallback for backwards compatibility
if err := generateDependencyFile(files, "requirements.txt", effectiveDeps.Pip, effectiveDeps.Paths.Pip); err != nil {
return nil, fmt.Errorf("failed to generate pip requirements: %w", err)
}

// Generate conda requirements
if err := generateDependencyFile(files, "conda_pkglist.txt", config.Dependencies.Conda, config.Dependencies.Paths.Conda); err != nil {
if err := generateDependencyFile(files, "conda_pkglist.txt", effectiveDeps.Conda, effectiveDeps.Paths.Conda); err != nil {
return nil, fmt.Errorf("failed to generate conda requirements: %w", err)
}

// Generate apt requirements
if err := generateDependencyFile(files, "pkglist.txt", config.Dependencies.Apt, config.Dependencies.Paths.Apt); err != nil {
if err := generateDependencyFile(files, "pkglist.txt", effectiveDeps.Apt, effectiveDeps.Paths.Apt); err != nil {
return nil, fmt.Errorf("failed to generate apt requirements: %w", err)
}

// Generate shell commands file
if len(config.Deployment.ShellCommands) > 0 {
files["shell_commands.sh"] = generateShellCommandsContent(config.Deployment.ShellCommands)
// Generate shell commands file (from runtime or deprecated deployment section)
if shellCmds := config.GetEffectiveShellCommands(); len(shellCmds) > 0 {
files["shell_commands.sh"] = generateShellCommandsContent(shellCmds)
}

// Generate pre-build commands file
if len(config.Deployment.PreBuildCommands) > 0 {
files["pre_build_commands.sh"] = generateShellCommandsContent(config.Deployment.PreBuildCommands)
// Generate pre-build commands file (from runtime or deprecated deployment section)
if preBuildCmds := config.GetEffectivePreBuildCommands(); len(preBuildCmds) > 0 {
files["pre_build_commands.sh"] = generateShellCommandsContent(preBuildCmds)
}

return files, nil
}

// generateDependencyFile handles both inline dependencies and file paths
func generateDependencyFile(files map[string]string, fileName string, deps map[string]string, filePath string) error {
// Check if both are specified
if len(deps) > 0 && filePath != "" {
return fmt.Errorf("both %s and dependencies specified in config - please specify only one", fileName)
// Supports both:
// 1. _file_relative_path key in the deps map (recommended)
// 2. deprecatedFilePath from [dependencies.paths] (backwards compatible)
// If both are specified, _file_relative_path takes precedence
// The file is read as a base and inline packages are merged on top
func generateDependencyFile(files map[string]string, fileName string, deps map[string]string, deprecatedFilePath string) error {
// Get file path - new _file_relative_path key takes precedence over deprecated Paths
filePath := projectconfig.GetFilePath(deps)
if filePath == "" {
filePath = deprecatedFilePath // Fall back to deprecated [dependencies.paths] if new key not set
}
packages := projectconfig.GetPackages(deps)

// If a file path is specified, read and use that file
// Start with base dependencies from file (if specified)
baseDeps := make(map[string]string)
if filePath != "" {
// Check if the file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return fmt.Errorf("the specified file '%s' was not found", filePath)
}

// Read the file content
// Read and parse the file content
content, err := os.ReadFile(filePath) //nolint:gosec // File path from user's project configuration
if err != nil {
return fmt.Errorf("failed to read file '%s': %w", filePath, err)
}

files[fileName] = string(content)
return nil
// Parse the file into a dependency map
baseDeps = parseRequirementsFile(string(content))
}

// Merge inline packages on top (inline wins per-package)
for pkg, ver := range packages {
baseDeps[pkg] = ver
}

// Otherwise, generate from inline dependencies
if len(deps) > 0 {
files[fileName] = generateRequirementsContent(deps)
// Generate the output if we have any dependencies
if len(baseDeps) > 0 {
files[fileName] = generateRequirementsContent(baseDeps)
}

return nil
}

// parseRequirementsFile parses a requirements.txt style file into a dependency map
func parseRequirementsFile(content string) map[string]string {
deps := make(map[string]string)
lines := strings.Split(content, "\n")

for _, line := range lines {
line = strings.TrimSpace(line)
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}

// Handle various formats: pkg==1.0, pkg>=1.0, pkg, git+https://...
if strings.HasPrefix(line, "git+") || strings.HasPrefix(line, "http://") || strings.HasPrefix(line, "https://") {
// URL-based dependency - use the whole line as the key
deps[line] = ""
} else if idx := strings.IndexAny(line, "=<>!~"); idx != -1 {
// Package with version specifier
pkg := line[:idx]
ver := line[idx:]
deps[pkg] = ver
} else {
// Package without version
deps[line] = ""
}
}

return deps
}

// generateRequirementsContent creates content for a requirements file
func generateRequirementsContent(deps map[string]string) string {
// Sort package names for deterministic output
Expand Down
Loading
Loading