diff --git a/cmd/project.go b/cmd/project.go index 80e5832..01b44ba 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -16,5 +16,5 @@ func init() { projectCmd.AddCommand(project.NewProjectCmd) projectCmd.AddCommand(project.StartProjectCmd) projectCmd.AddCommand(project.DeployProjectCmd) - // projectCmd.AddCommand(project.BuildProjectCmd) + projectCmd.AddCommand(project.BuildProjectCmd) } diff --git a/cmd/project/exampleDockerfile b/cmd/project/exampleDockerfile new file mode 100644 index 0000000..1c4082c --- /dev/null +++ b/cmd/project/exampleDockerfile @@ -0,0 +1,28 @@ +# AUTOGENERATED Dockerfile using runpodctl project build + +# Base image -> https://github.com/runpod/containers/blob/main/official-templates/base/Dockerfile +# DockerHub -> https://hub.docker.com/r/runpod/base/tags +FROM <> + +# The base image comes with many system dependencies pre-installed to help you get started quickly. +# Please refer to the base image's Dockerfile for more information before adding additional dependencies. +# IMPORTANT: The base image overrides the default huggingface cache location. + +# System dependencies +# if you need additional system dependencies beyond those included in the base image, feel free to add them here +# but be aware those changes will not be reflected in the development pod +# unless you override the base image in runpod.toml to an image that includes them. + +# Python dependencies +COPY <> /requirements.txt +RUN python<> -m pip install --upgrade pip && \ + python<> -m pip install --upgrade -r /requirements.txt --no-cache-dir && \ + rm /requirements.txt + +# NOTE: The base image comes with multiple Python versions pre-installed. +# It is recommended to specify the version of Python when running your code. + +# Add src files (Worker Template) +ADD src . +<> +CMD python<> -u <> \ No newline at end of file diff --git a/cmd/project/functions.go b/cmd/project/functions.go index b519cfc..fc1654a 100644 --- a/cmd/project/functions.go +++ b/cmd/project/functions.go @@ -23,6 +23,9 @@ var starterTemplates embed.FS //go:embed example.toml var tomlTemplate embed.FS +//go:embed exampleDockerfile +var dockerfileTemplate embed.FS + const basePath string = "starter_templates" func baseDockerImage(cudaVersion string) string { @@ -30,7 +33,7 @@ func baseDockerImage(cudaVersion string) string { } func copyFiles(files fs.FS, source string, dest string) error { - return fs.WalkDir(starterTemplates, source, func(path string, d fs.DirEntry, err error) error { + return fs.WalkDir(files, source, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -43,7 +46,7 @@ func copyFiles(files fs.FS, source string, dest string) error { if d.IsDir() { return os.MkdirAll(newPath, os.ModePerm) } else { - content, err := fs.ReadFile(starterTemplates, path) + content, err := fs.ReadFile(files, path) if err != nil { return err } @@ -212,6 +215,13 @@ func mapToApiEnv(env map[string]string) []*api.PodEnv { } return podEnv } +func formatAsDockerEnv(env map[string]string) string { + result := "" + for k, v := range env { + result += fmt.Sprintf("ENV %s=%s\n", k, v) + } + return result +} func startProject(networkVolumeId string) error { //parse project toml @@ -475,3 +485,32 @@ func deployProject(networkVolumeId string) (endpointId string, err error) { } return deployedEndpointId, nil } + +func buildProjectDockerfile() { + //parse project toml + config := loadProjectConfig() + projectConfig := config.Get("project").(*toml.Tree) + runtimeConfig := config.Get("runtime").(*toml.Tree) + //build Dockerfile + dockerfileBytes, _ := dockerfileTemplate.ReadFile("exampleDockerfile") + dockerfile := string(dockerfileBytes) + //base image: from toml + dockerfile = strings.ReplaceAll(dockerfile, "<>", projectConfig.Get("base_image").(string)) + //pip requirements + dockerfile = strings.ReplaceAll(dockerfile, "<>", runtimeConfig.Get("requirements_path").(string)) + dockerfile = strings.ReplaceAll(dockerfile, "<>", runtimeConfig.Get("python_version").(string)) + //cmd: start handler + dockerfile = strings.ReplaceAll(dockerfile, "<>", runtimeConfig.Get("handler_path").(string)) + if includeEnvInDockerfile { + dockerEnv := formatAsDockerEnv(createEnvVars(config)) + dockerfile = strings.ReplaceAll(dockerfile, "<>", "\n"+dockerEnv) + } else { + dockerfile = strings.ReplaceAll(dockerfile, "<>", "") + } + //save to Dockerfile in project directory + projectFolder, _ := os.Getwd() + dockerfilePath := filepath.Join(projectFolder, "Dockerfile") + os.WriteFile(dockerfilePath, []byte(dockerfile), 0644) + fmt.Printf("Dockerfile created at %s\n", dockerfilePath) + +} diff --git a/cmd/project/project.go b/cmd/project/project.go index 5020716..ee8e664 100644 --- a/cmd/project/project.go +++ b/cmd/project/project.go @@ -15,6 +15,7 @@ var modelType string var modelName string var initCurrentDir bool var setDefaultNetworkVolume bool +var includeEnvInDockerfile bool const inputPromptPrefix string = " > " @@ -46,10 +47,6 @@ func promptChoice(message string, choices []string, defaultChoice string) string } return s } -func setDefaultNetVolume(projectId string, networkVolumeId string) { - viper.Set(fmt.Sprintf("project_volumes.%s", projectId), networkVolumeId) - viper.WriteConfig() -} func selectNetworkVolume() (networkVolumeId string, err error) { networkVolumes, err := api.GetNetworkVolumes() @@ -86,6 +83,40 @@ func selectNetworkVolume() (networkVolumeId string, err error) { networkVolumeId = options[i].Value return networkVolumeId, nil } +func selectStarterTemplate() (template string, err error) { + type StarterTemplateOption struct { + Name string // The string to display + Value string // The actual value to use + } + templates, err := starterTemplates.ReadDir("starter_templates") + if err != nil { + fmt.Println("Something went wrong trying to fetch starter templates") + fmt.Println(err) + return "", err + } + promptTemplates := &promptui.SelectTemplates{ + Label: inputPromptPrefix + "{{ . }}", + Active: ` {{ "●" | cyan }} {{ .Name | cyan }}`, + Inactive: ` {{ .Name | white }}`, + Selected: ` {{ .Name | white }}`, + } + options := []StarterTemplateOption{} + for _, template := range templates { + options = append(options, StarterTemplateOption{Name: template.Name(), Value: template.Name()}) + } + getStarterTemplate := promptui.Select{ + Label: "Select a Starter Template:", + Items: options, + Templates: promptTemplates, + } + i, _, err := getStarterTemplate.Run() + if err != nil { + //ctrl c for example + return "", err + } + template = options[i].Value + return template, nil +} // Define a struct that holds the display string and the corresponding value type NetVolOption struct { @@ -105,17 +136,24 @@ var NewProjectCmd = &cobra.Command{ } else { fmt.Println("Project name: " + projectName) } + if modelType == "" { + template, err := selectStarterTemplate() + modelType = template + if err != nil { + modelType = "" + } + } cudaVersion := promptChoice("Select a CUDA version, or press enter to use the default", []string{"11.1.1", "11.8.0", "12.1.0"}, "11.8.0") pythonVersion := promptChoice("Select a Python version, or press enter to use the default", []string{"3.8", "3.9", "3.10", "3.11"}, "3.10") - fmt.Printf(` Project Summary: - Project Name: %s + - Starter Template: %s - CUDA Version: %s - Python Version: %s - `, projectName, cudaVersion, pythonVersion) + `, projectName, modelType, cudaVersion, pythonVersion) fmt.Println() fmt.Println("The project will be created in the current directory.") //TODO confirm y/n @@ -136,7 +174,16 @@ var StartProjectCmd = &cobra.Command{ config := loadProjectConfig() projectId := config.GetPath([]string{"project", "uuid"}).(string) networkVolumeId := viper.GetString(fmt.Sprintf("project_volumes.%s", projectId)) - if setDefaultNetworkVolume || networkVolumeId == "" { + cachedNetVolExists := false + networkVolumes, err := api.GetNetworkVolumes() + if err == nil { + for _, networkVolume := range networkVolumes { + if networkVolume.Id == networkVolumeId { + cachedNetVolExists = true + } + } + } + if setDefaultNetworkVolume || networkVolumeId == "" || !cachedNetVolExists { netVolId, err := selectNetworkVolume() networkVolumeId = netVolId viper.Set(fmt.Sprintf("project_volumes.%s", projectId), networkVolumeId) @@ -175,22 +222,33 @@ var DeployProjectCmd = &cobra.Command{ }, } -// var BuildProjectCmd = &cobra.Command{ -// Use: "build", -// Args: cobra.ExactArgs(0), -// Short: "build Docker image for current project", -// Long: "build a Docker image for the Runpod project in the current folder", -// Run: func(cmd *cobra.Command, args []string) { -// //parse project toml -// //build Dockerfile -// //base image: from toml -// //run setup.sh for system deps -// //pip install requirements -// //cmd: start handler -// //docker build -// //print next steps -// }, -// } +var BuildProjectCmd = &cobra.Command{ + Use: "build", + Args: cobra.ExactArgs(0), + Short: "build Dockerfile for current project", + Long: "build a Dockerfile for the Runpod project in the current folder", + Run: func(cmd *cobra.Command, args []string) { + buildProjectDockerfile() + // config := loadProjectConfig() + // projectConfig := config.Get("project").(*toml.Tree) + // projectId := projectConfig.Get("uuid").(string) + // projectName := projectConfig.Get("name").(string) + // //print next steps + // fmt.Println("Next steps:") + // fmt.Println() + // suggestedDockerTag := fmt.Sprintf("runpod-sls-worker-%s-%s:0.1", projectName, projectId) + // //docker build + // fmt.Println("# Build Docker image") + // fmt.Printf("docker build -t %s .\n", suggestedDockerTag) + // //dockerhub push + // fmt.Println("# Push Docker image to a container registry such as Dockerhub") + // fmt.Printf("docker push %s\n", suggestedDockerTag) + // //go to runpod url and deploy + // fmt.Println() + // fmt.Println("Deploy docker image as a serverless endpoint on Runpod") + // fmt.Println("https://www.runpod.io/console/serverless") + }, +} func init() { NewProjectCmd.Flags().StringVarP(&projectName, "name", "n", "", "project name") @@ -199,5 +257,6 @@ func init() { NewProjectCmd.Flags().BoolVarP(&initCurrentDir, "init", "i", false, "use the current directory as the project directory") StartProjectCmd.Flags().BoolVar(&setDefaultNetworkVolume, "select-volume", false, "select a new default network volume for current project") - DeployProjectCmd.Flags().BoolVar(&setDefaultNetworkVolume, "select-volume", false, "select a new default network volume for current project") + BuildProjectCmd.Flags().BoolVar(&includeEnvInDockerfile, "include-env", false, "include environment variables from runpod.toml in generated Dockerfile") + } diff --git a/cmd/project/starter_templates/default/.runpodignore b/cmd/project/starter_templates/default/.runpodignore index f14a0ac..e5f1925 100644 --- a/cmd/project/starter_templates/default/.runpodignore +++ b/cmd/project/starter_templates/default/.runpodignore @@ -1,3 +1,4 @@ # Similar to .gitignore # Matches will not be synce to the development pod or cause the development pod to reload. +Dockerfile diff --git a/cmd/project/starter_templates/llama2/.runpodignore b/cmd/project/starter_templates/llama2/.runpodignore index f14a0ac..e5f1925 100644 --- a/cmd/project/starter_templates/llama2/.runpodignore +++ b/cmd/project/starter_templates/llama2/.runpodignore @@ -1,3 +1,4 @@ # Similar to .gitignore # Matches will not be synce to the development pod or cause the development pod to reload. +Dockerfile diff --git a/makefile b/makefile index e7adb4c..7253fd0 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,6 @@ .PHONY: proto dev: - env GOOS=windows GOARCH=amd64 go build -ldflags "-X 'main.Version=1.0.0'" -o bin/runpodctl.exe . + env GOOS=darwin GOARCH=arm64 go build -ldflags "-X 'main.Version=1.0.0'" -o bin/runpodctl . lint: golangci-lint run