feat(build): print if no services to build, build dependencies --dep, check build configuration --check

This commit is contained in:
Pasha Sviderski
2025-10-30 20:54:15 +10:00
parent 5c4f49b685
commit 65c3e0a68d
5 changed files with 98 additions and 33 deletions
+4 -1
View File
@@ -66,7 +66,10 @@ func runBuild(ctx context.Context, uncli *cli.CLI, opts cli.BuildOptions) error
} }
} }
servicesToBuild := cli.GetServicesThatNeedBuild(project) servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.Services, false)
if err != nil {
return fmt.Errorf("determine services to build: %w", err)
}
if len(servicesToBuild) == 0 { if len(servicesToBuild) == 0 {
fmt.Println("No services to build.") fmt.Println("No services to build.")
+18 -17
View File
@@ -17,6 +17,8 @@ import (
type buildOptions struct { type buildOptions struct {
buildArgs []string buildArgs []string
check bool
deps bool
files []string files []string
noCache bool noCache bool
profiles []string profiles []string
@@ -44,10 +46,14 @@ func NewCBuildCommand() *cobra.Command {
} }
cmd.Flags().StringArrayVar(&opts.buildArgs, "build-arg", nil, cmd.Flags().StringArrayVar(&opts.buildArgs, "build-arg", nil,
"Set a build-time variable to pass to service Dockerfiles. Only used if declared with ARG.\n"+ "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") "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, cmd.Flags().StringSliceVarP(&opts.files, "file", "f", nil,
"One or more Compose files to build (default compose.yaml)") "One or more Compose files to build. (default compose.yaml)")
cmd.Flags().BoolVar(&opts.noCache, "no-cache", false, cmd.Flags().BoolVar(&opts.noCache, "no-cache", false,
"Do not use cache when building images.") "Do not use cache when building images.")
cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil, cmd.Flags().StringSliceVarP(&opts.profiles, "profile", "p", nil,
@@ -60,32 +66,27 @@ func NewCBuildCommand() *cobra.Command {
// projectOptsFromCBuildOpts returns the project options for the Compose file(s). // projectOptsFromCBuildOpts returns the project options for the Compose file(s).
func projectOptsFromCBuildOpts(opts buildOptions) []composecli.ProjectOptionsFn { func projectOptsFromCBuildOpts(opts buildOptions) []composecli.ProjectOptionsFn {
var projectOpts []composecli.ProjectOptionsFn var projOpts []composecli.ProjectOptionsFn
if len(opts.profiles) > 0 { if len(opts.profiles) > 0 {
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...)) projOpts = append(projOpts, composecli.WithDefaultProfiles(opts.profiles...))
} }
return projectOpts return projOpts
} }
// runCBuild parses the Compose file(s) and builds the images for selected services. // runCBuild parses the Compose file(s) and builds the images for selected services.
func runCBuild(ctx context.Context, _ *cli.CLI, opts buildOptions) error { func runCBuild(ctx context.Context, _ *cli.CLI, opts buildOptions) error {
projectOpts := projectOptsFromCBuildOpts(opts) projOpts := projectOptsFromCBuildOpts(opts)
project, err := compose.LoadProject(ctx, opts.files, projectOpts...) project, err := compose.LoadProject(ctx, opts.files, projOpts...)
if err != nil { if err != nil {
return fmt.Errorf("load compose file(s): %w", err) return fmt.Errorf("load compose file(s): %w", err)
} }
if len(opts.services) > 0 { servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.services, opts.deps)
project, err = project.WithSelectedServices(opts.services) if err != nil {
if err != nil { return fmt.Errorf("determine services to build: %w", err)
return fmt.Errorf("select services: %w", err)
}
} }
servicesToBuild := cli.GetServicesThatNeedBuild(project)
if len(servicesToBuild) == 0 { if len(servicesToBuild) == 0 {
fmt.Println("No services to build.") fmt.Println("No services to build.")
return nil return nil
@@ -105,6 +106,8 @@ func runCBuild(ctx context.Context, _ *cli.CLI, opts buildOptions) error {
composeService := composev2.NewComposeService(dockerCli) composeService := composev2.NewComposeService(dockerCli)
buildOpts := composeapi.BuildOptions{ buildOpts := composeapi.BuildOptions{
Args: composetypes.NewMappingWithEquals(opts.buildArgs), Args: composetypes.NewMappingWithEquals(opts.buildArgs),
Check: opts.check,
Deps: opts.deps,
NoCache: opts.noCache, NoCache: opts.noCache,
Pull: opts.pull, Pull: opts.pull,
Services: opts.services, Services: opts.services,
@@ -114,7 +117,5 @@ func runCBuild(ctx context.Context, _ *cli.CLI, opts buildOptions) error {
return fmt.Errorf("build services: %w", err) return fmt.Errorf("build services: %w", err)
} }
fmt.Println("Build completed successfully.")
return nil return nil
} }
+9 -6
View File
@@ -68,20 +68,20 @@ func NewDeployCommand() *cobra.Command {
// projectOpts returns the project options for the Compose file(s). // projectOpts returns the project options for the Compose file(s).
func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn { func projectOpts(opts deployOptions) []composecli.ProjectOptionsFn {
projectOpts := []composecli.ProjectOptionsFn{} var projOpts []composecli.ProjectOptionsFn
if len(opts.profiles) > 0 { if len(opts.profiles) > 0 {
projectOpts = append(projectOpts, composecli.WithDefaultProfiles(opts.profiles...)) projOpts = append(projOpts, composecli.WithDefaultProfiles(opts.profiles...))
} }
return projectOpts return projOpts
} }
// runDeploy parses the Compose file(s) and deploys the services. // runDeploy parses the Compose file(s) and deploys the services.
func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error { func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
projectOpts := projectOpts(opts) projOpts := projectOpts(opts)
project, err := compose.LoadProject(ctx, opts.files, projectOpts...) project, err := compose.LoadProject(ctx, opts.files, projOpts...)
if err != nil { if err != nil {
return fmt.Errorf("load compose file(s): %w", err) return fmt.Errorf("load compose file(s): %w", err)
} }
@@ -94,7 +94,10 @@ func runDeploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
} }
} }
servicesToBuild := cli.GetServicesThatNeedBuild(project) servicesToBuild, err := cli.ServicesThatNeedBuild(project, opts.services, false)
if err != nil {
return fmt.Errorf("determine services to build: %w", err)
}
if len(servicesToBuild) > 0 { if len(servicesToBuild) > 0 {
if opts.noBuild { if opts.noBuild {
+65 -8
View File
@@ -6,8 +6,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
composetypes "github.com/compose-spec/compose-go/v2/types" composetypes "github.com/compose-spec/compose-go/v2/types"
mapset "github.com/deckarep/golang-set/v2"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/build"
@@ -27,16 +29,71 @@ type BuildOptions struct {
NoCache bool NoCache bool
} }
// GetServicesThatNeedBuild returns a map of services that require building // ServicesThatNeedBuild returns a map of services that require building.
func GetServicesThatNeedBuild(project *composetypes.Project) map[string]composetypes.ServiceConfig { // deps indicates whether to include services that are dependencies of the selected services.
// Implementation is based on the logic from docker/compose/v2/pkg/compose/build.go.
func ServicesThatNeedBuild(
project *composetypes.Project, selectedServices []string, deps bool,
) (map[string]composetypes.ServiceConfig, error) {
servicesToBuild := make(map[string]composetypes.ServiceConfig, len(project.Services)) servicesToBuild := make(map[string]composetypes.ServiceConfig, len(project.Services))
for serviceName, service := range project.Services {
if service.Build == nil { var policy composetypes.DependencyOption = composetypes.IgnoreDependencies
continue if deps {
} policy = composetypes.IncludeDependencies
servicesToBuild[serviceName] = service
} }
return servicesToBuild
// Also include services used as build.additional_contexts with service: prefix.
selectedServices = includeAdditionalContextsServices(project, selectedServices)
// Some build dependencies we just introduced may not be enabled, enable them.
var err error
project, err = project.WithServicesEnabled(selectedServices...)
if err != nil {
return nil, err
}
project, err = project.WithSelectedServices(selectedServices)
if err != nil {
return nil, err
}
err = project.ForEachService(selectedServices, func(serviceName string, service *composetypes.ServiceConfig) error {
if service.Build != nil {
servicesToBuild[serviceName] = *service
}
return nil
}, policy)
for serviceName, service := range project.Services {
if service.Build != nil {
servicesToBuild[serviceName] = service
}
}
return servicesToBuild, nil
}
// includeAdditionalContextsServices adds services referenced in build.additional_contexts to the list
// of selected services.
func includeAdditionalContextsServices(project *composetypes.Project, selectedServices []string) []string {
servicesWithDependencies := mapset.NewSet(selectedServices...)
for _, service := range selectedServices {
s, ok := project.Services[service]
if !ok {
s = project.DisabledServices[service]
}
if s.Build != nil {
for _, target := range s.Build.AdditionalContexts {
if name, found := strings.CutPrefix(target, composetypes.ServicePrefix); found {
servicesWithDependencies.Add(name)
}
}
}
}
if servicesWithDependencies.Cardinality() > len(selectedServices) {
return includeAdditionalContextsServices(project, servicesWithDependencies.ToSlice())
}
return servicesWithDependencies.ToSlice()
} }
// BuildServices builds the services defined in the provided map. // BuildServices builds the services defined in the provided map.
+2 -1
View File
@@ -97,7 +97,8 @@ func TestComposeBuild(t *testing.T) {
}), }),
) )
require.NoError(t, err) require.NoError(t, err)
servicesToBuild := cliInternal.GetServicesThatNeedBuild(project) servicesToBuild, err := cliInternal.ServicesThatNeedBuild(project, nil, false)
require.NoError(t, err)
serviceImage1 := fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort) serviceImage1 := fmt.Sprintf("127.0.0.1:%d/service-first", registryHostPort)
serviceImage2 := fmt.Sprintf("127.0.0.1:%d/service-second:version2", registryHostPort) serviceImage2 := fmt.Sprintf("127.0.0.1:%d/service-second:version2", registryHostPort)
t.Cleanup(func() { t.Cleanup(func() {