package main import ( "context" "fmt" composecli "github.com/compose-spec/compose-go/v2/cli" "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli/completion" "github.com/psviderski/uncloud/pkg/client/compose" "github.com/spf13/cobra" ) type buildOptions struct { cli.BuildServicesOptions files []string profiles []string } // NewBuildCommand creates a new command to build images for services from a Compose file. func NewBuildCommand() *cobra.Command { opts := buildOptions{} cmd := &cobra.Command{ Use: "build [FLAGS] [SERVICE...]", Short: "Build services from a Compose file.", Long: `Build images for services from a Compose file using local Docker. By default, built images remain on the local Docker host. Use --push to upload them to cluster machines or --push-registry to upload them to external registries.`, Example: ` # Build all services that have a build section in compose.yaml. uc build # Build specific services that have a build section. uc build web api # Build services and push images to all cluster machines or service x-machines if specified. uc build --push # Build services and push images to specific machines. uc build --push -m machine1,machine2 # Build services and push images to external registries (e.g., Docker Hub). uc build --push-registry # Build services with build arguments, pull newer base images before building, and don't use cache. uc build --build-arg NODE_VERSION=24 --build-arg ENV=production --no-cache --pull`, RunE: func(cmd *cobra.Command, args []string) error { uncli := cmd.Context().Value("cli").(*cli.CLI) opts.Services = args return runBuild(cmd.Context(), uncli, opts) }, GroupID: "service", ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { return completion.ComposeServices(cmd.Context(), args, toComplete, opts.files, opts.profiles) }, } cmd.Flags().StringArrayVar(&opts.BuildArgs, "build-arg", nil, "Set a build-time variable for services. Used in Dockerfiles that declare the variable with ARG.\n"+ "Can be specified multiple times. Format: --build-arg VAR=VALUE") cmd.Flags().BoolVar(&opts.Check, "check", false, "Check the build configuration for services without building them.") cmd.Flags().BoolVar(&opts.Deps, "deps", false, "Also build services declared as dependencies of the selected services.") cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil, "One or more Compose files to build. (default compose.yaml)") cmd.Flags().StringSliceVarP(&opts.Machines, "machine", "m", nil, "Machine names or IDs to push the built images to (requires --push).\n"+ "Can be specified multiple times or as a comma-separated list. (default is all machines or x-machines)") cmd.Flags().BoolVar(&opts.NoCache, "no-cache", false, "Do not use cache when building images.") cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil, "One or more Compose profiles to enable.") cmd.Flags().BoolVar(&opts.Pull, "pull", false, "Always attempt to pull newer versions of base images before building.") cmd.Flags().BoolVar(&opts.PushCluster, "push", false, "Upload the built images to cluster machines after building.\n"+ "Use --machine to specify which machines. (default is all machines)") cmd.Flags().BoolVar(&opts.PushRegistry, "push-registry", false, "Upload the built images to external registries (e.g., Docker Hub) after building.") completion.MachinesFlag(cmd) return cmd } // runBuild parses the Compose file(s) and builds the images for selected services. func runBuild(ctx context.Context, uncli *cli.CLI, opts buildOptions) error { // Validate push flags. if opts.PushCluster && opts.PushRegistry { return fmt.Errorf("cannot specify both --push and --push-registry: choose one push target") } machines := cli.ExpandCommaSeparatedValues(opts.Machines) // Special handling for an explicit "all" keyword to push to all machines. if len(machines) == 1 && machines[0] == "all" { machines = nil } opts.Machines = machines project, err := compose.LoadProject(ctx, opts.files, composecli.WithDefaultProfiles(opts.profiles...)) if err != nil { return fmt.Errorf("load compose file(s): %w", err) } uncli.SetClusterContextIfUnset(compose.ClusterContext(project)) servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, opts.Deps) if err != nil { return fmt.Errorf("determine services to build: %w", err) } if len(servicesToBuild) == 0 { fmt.Println("No services to build.") return nil } return uncli.BuildServices(ctx, project, opts.BuildServicesOptions) }