swap 'pkg push' for 'pkg build --push', keeping 'pkg push' as deprecated but still working (#4141)

Signed-off-by: Avi Deitcher <avi@deitcher.net>
This commit is contained in:
Avi Deitcher 2025-07-04 18:00:28 +03:00 committed by GitHub
parent 2b4687338b
commit c0c5668116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 241 additions and 226 deletions

View File

@ -75,9 +75,11 @@ func pkgCmd() *cobra.Command {
},
}
cmd.AddCommand(pkgBuildCmd())
// because there is an alias 'pkg push' for 'pkg build --push', we need to add the build command first
buildCmd := pkgBuildCmd()
cmd.AddCommand(buildCmd)
cmd.AddCommand(pkgBuilderCmd())
cmd.AddCommand(pkgPushCmd())
cmd.AddCommand(pkgPushCmd(buildCmd))
cmd.AddCommand(pkgShowTagCmd())
cmd.AddCommand(pkgManifestCmd())
cmd.AddCommand(pkgRemoteTagCmd())

View File

@ -20,21 +20,21 @@ const (
)
// some logic clarification:
// pkg build - builds unless is in cache or published in registry
// pkg build --pull - builds unless is in cache or published in registry; pulls from registry if not in cache
// pkg build --force - always builds even if is in cache or published in registry
// pkg build --force --pull - always builds even if is in cache or published in registry; --pull ignored
// pkg push - always builds unless is in cache
// pkg push --force - always builds even if is in cache
// pkg push --nobuild - skips build; if not in cache, fails
// pkg push --nobuild --force - nonsensical
// pkg build - builds unless is in cache or published in registry
// pkg build --pull - builds unless is in cache or published in registry; pulls from registry if not in cache
// pkg build --force - always builds even if is in cache or published in registry
// pkg build --force --pull - always builds even if is in cache or published in registry; --pull ignored
// pkg build --push - always builds unless is in cache or published in registry; pushes to registry
// pkg build --push --force - always builds even if is in cache
// pkg build --push --nobuild - skips build; if not in cache, fails
// pkg build --push --nobuild --force - nonsensical
// pkg push - equivalent to pkg build --push
// addCmdRunPkgBuildPush adds the RunE function and flags to a cobra.Command
// for "pkg build" or "pkg push".
func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
func pkgBuildCmd() *cobra.Command {
var (
force bool
pull bool
push bool
ignoreCache bool
docker bool
platforms string
@ -53,220 +53,228 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
progress string
ssh []string
)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...)
if err != nil {
return err
}
if nobuild && force {
return errors.New("flags -force and -nobuild conflict")
}
if pull && force {
return errors.New("flags -force and -pull conflict")
}
var opts []pkglib.BuildOpt
if force {
opts = append(opts, pkglib.WithBuildForce())
}
if ignoreCache {
opts = append(opts, pkglib.WithBuildIgnoreCache())
}
if pull {
opts = append(opts, pkglib.WithBuildPull())
}
opts = append(opts, pkglib.WithBuildCacheDir(cacheDir.String()))
if withPush {
opts = append(opts, pkglib.WithBuildPush())
if nobuild {
opts = append(opts, pkglib.WithBuildSkip())
}
if release != "" {
opts = append(opts, pkglib.WithRelease(release))
}
if manifest {
opts = append(opts, pkglib.WithBuildManifest())
}
}
if docker {
opts = append(opts, pkglib.WithBuildTargetDockerCache())
}
if sbomScanner != "false" {
opts = append(opts, pkglib.WithBuildSbomScanner(sbomScanner))
}
opts = append(opts, pkglib.WithDockerfile(dockerfile))
// read any build arg files
var buildArgs []string
for _, filename := range buildArgFiles {
f, err := os.Open(filename)
cmd := &cobra.Command{
Use: "build",
Short: "build an OCI package from a directory with a yaml configuration file",
Long: `Build an OCI package from a directory with a yaml configuration file.
'path' specifies the path to the package source directory.
`,
Example: ` linuxkit pkg build [options] pkg/dir/`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
pkgs, err := pkglib.NewFromConfig(pkglibConfig, args...)
if err != nil {
return fmt.Errorf("error opening build args file %s: %w", filename, err)
return err
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
buildArgs = append(buildArgs, scanner.Text())
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading build args file %s: %w", filename, err)
}
}
opts = append(opts, pkglib.WithBuildArgs(buildArgs))
// skipPlatformsMap contains platforms that should be skipped
skipPlatformsMap := make(map[string]bool)
if skipPlatforms != "" {
for _, platform := range strings.Split(skipPlatforms, ",") {
parts := strings.SplitN(platform, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[0] != "linux" || parts[1] == "" {
return fmt.Errorf("invalid target platform specification '%s'", platform)
}
skipPlatformsMap[strings.Trim(parts[1], " ")] = true
if nobuild && force {
return errors.New("flags -force and -nobuild conflict")
}
}
// if requested specific platforms, build those. If not, then we will
// retrieve the defaults in the loop over each package.
var plats []imagespec.Platform
// don't allow the use of --skip-platforms with --platforms
if platforms != "" && skipPlatforms != "" {
return errors.New("--skip-platforms and --platforms may not be used together")
}
// process the platforms if provided
if platforms != "" {
for _, p := range strings.Split(platforms, ",") {
parts := strings.SplitN(p, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", p)
os.Exit(1)
}
plats = append(plats, imagespec.Platform{OS: parts[0], Architecture: parts[1]})
if pull && force {
return errors.New("flags -force and -pull conflict")
}
}
// build the builders map
buildersMap := map[string]string{}
// look for builders env var
buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap)
if err != nil {
return fmt.Errorf("error in environment variable %s: %w", buildersEnvVar, err)
}
// any CLI options override env var
buildersMap, err = buildPlatformBuildersMap(builders, buildersMap)
if err != nil {
return fmt.Errorf("error in --builders flag: %w", err)
}
if builderConfig != "" {
if _, err := os.Stat(builderConfig); err != nil {
return fmt.Errorf("error reading builder config file %s: %w", builderConfig, err)
var opts []pkglib.BuildOpt
if force {
opts = append(opts, pkglib.WithBuildForce())
}
if ignoreCache {
opts = append(opts, pkglib.WithBuildIgnoreCache())
}
if pull {
opts = append(opts, pkglib.WithBuildPull())
}
opts = append(opts, pkglib.WithBuildBuilderConfig(builderConfig))
}
opts = append(opts, pkglib.WithBuildBuilders(buildersMap))
opts = append(opts, pkglib.WithBuildBuilderImage(builderImage))
opts = append(opts, pkglib.WithBuildBuilderRestart(builderRestart))
opts = append(opts, pkglib.WithProgress(progress))
if len(ssh) > 0 {
opts = append(opts, pkglib.WithSSH(ssh))
}
if len(registryCreds) > 0 {
registryCredMap := make(map[string]spec.RegistryAuth)
for _, cred := range registryCreds {
parts := strings.SplitN(cred, "=", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid registry auth specification '%s'", cred)
opts = append(opts, pkglib.WithBuildCacheDir(cacheDir.String()))
if push {
opts = append(opts, pkglib.WithBuildPush())
if nobuild {
opts = append(opts, pkglib.WithBuildSkip())
}
registryPart := strings.TrimSpace(parts[0])
authPart := strings.TrimSpace(parts[1])
var auth spec.RegistryAuth
// if the auth is a token, we don't need a username
credParts := strings.SplitN(authPart, ":", 2)
var userPart, credPart string
userPart = strings.TrimSpace(credParts[0])
if len(credParts) == 2 {
credPart = strings.TrimSpace(credParts[1])
if release != "" {
opts = append(opts, pkglib.WithRelease(release))
}
if manifest {
opts = append(opts, pkglib.WithBuildManifest())
}
}
if docker {
opts = append(opts, pkglib.WithBuildTargetDockerCache())
}
if sbomScanner != "false" {
opts = append(opts, pkglib.WithBuildSbomScanner(sbomScanner))
}
opts = append(opts, pkglib.WithDockerfile(dockerfile))
// read any build arg files
var buildArgs []string
for _, filename := range buildArgFiles {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("error opening build args file %s: %w", filename, err)
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
buildArgs = append(buildArgs, scanner.Text())
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading build args file %s: %w", filename, err)
}
}
opts = append(opts, pkglib.WithBuildArgs(buildArgs))
// skipPlatformsMap contains platforms that should be skipped
skipPlatformsMap := make(map[string]bool)
if skipPlatforms != "" {
for _, platform := range strings.Split(skipPlatforms, ",") {
parts := strings.SplitN(platform, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[0] != "linux" || parts[1] == "" {
return fmt.Errorf("invalid target platform specification '%s'", platform)
}
skipPlatformsMap[strings.Trim(parts[1], " ")] = true
}
}
// if requested specific platforms, build those. If not, then we will
// retrieve the defaults in the loop over each package.
var plats []imagespec.Platform
// don't allow the use of --skip-platforms with --platforms
if platforms != "" && skipPlatforms != "" {
return errors.New("--skip-platforms and --platforms may not be used together")
}
// process the platforms if provided
if platforms != "" {
for _, p := range strings.Split(platforms, ",") {
parts := strings.SplitN(p, "/", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
fmt.Fprintf(os.Stderr, "invalid target platform specification '%s'\n", p)
os.Exit(1)
}
plats = append(plats, imagespec.Platform{OS: parts[0], Architecture: parts[1]})
}
}
// build the builders map
buildersMap := map[string]string{}
// look for builders env var
buildersMap, err = buildPlatformBuildersMap(os.Getenv(buildersEnvVar), buildersMap)
if err != nil {
return fmt.Errorf("error in environment variable %s: %w", buildersEnvVar, err)
}
// any CLI options override env var
buildersMap, err = buildPlatformBuildersMap(builders, buildersMap)
if err != nil {
return fmt.Errorf("error in --builders flag: %w", err)
}
if builderConfig != "" {
if _, err := os.Stat(builderConfig); err != nil {
return fmt.Errorf("error reading builder config file %s: %w", builderConfig, err)
}
opts = append(opts, pkglib.WithBuildBuilderConfig(builderConfig))
}
opts = append(opts, pkglib.WithBuildBuilders(buildersMap))
opts = append(opts, pkglib.WithBuildBuilderImage(builderImage))
opts = append(opts, pkglib.WithBuildBuilderRestart(builderRestart))
opts = append(opts, pkglib.WithProgress(progress))
if len(ssh) > 0 {
opts = append(opts, pkglib.WithSSH(ssh))
}
if len(registryCreds) > 0 {
registryCredMap := make(map[string]spec.RegistryAuth)
for _, cred := range registryCreds {
parts := strings.SplitN(cred, "=", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid registry auth specification '%s'", cred)
}
registryPart := strings.TrimSpace(parts[0])
authPart := strings.TrimSpace(parts[1])
var auth spec.RegistryAuth
// if the auth is a token, we don't need a username
credParts := strings.SplitN(authPart, ":", 2)
var userPart, credPart string
userPart = strings.TrimSpace(credParts[0])
if len(credParts) == 2 {
credPart = strings.TrimSpace(credParts[1])
}
switch {
case len(registryPart) == 0:
return fmt.Errorf("invalid registry auth specification '%s', registry must not be blank", cred)
case len(credParts) == 2 && (len(userPart) == 0 || len(credPart) == 0):
return fmt.Errorf("invalid registry auth specification '%s', username and password must not be blank", cred)
case len(credParts) == 1 && len(userPart) == 0:
return fmt.Errorf("invalid registry auth specification '%s', token must not be blank", cred)
case len(credParts) == 2:
auth = spec.RegistryAuth{
Username: userPart,
Password: credPart,
}
case len(credParts) == 1:
auth = spec.RegistryAuth{
RegistryToken: authPart,
}
default:
return fmt.Errorf("invalid registry auth specification '%s'", cred)
}
registryCredMap[registryPart] = auth
}
opts = append(opts, pkglib.WithRegistryAuth(registryCredMap))
}
for _, p := range pkgs {
// things we need our own copies of
var (
pkgOpts = make([]pkglib.BuildOpt, len(opts))
pkgPlats = make([]imagespec.Platform, len(plats))
)
copy(pkgOpts, opts)
copy(pkgPlats, plats)
// unless overridden, platforms are specific to a package, so this needs to be inside the for loop
if len(pkgPlats) == 0 {
for _, a := range p.Arches() {
if _, ok := skipPlatformsMap[a]; ok {
continue
}
pkgPlats = append(pkgPlats, imagespec.Platform{OS: "linux", Architecture: a})
}
}
// if there are no platforms to build for, do nothing.
// note that this is *not* an error; we simply skip it
if len(pkgPlats) == 0 {
fmt.Printf("Skipping %s with no architectures to build\n", p.Tag())
continue
}
pkgOpts = append(pkgOpts, pkglib.WithBuildPlatforms(pkgPlats...))
var msg, action string
switch {
case len(registryPart) == 0:
return fmt.Errorf("invalid registry auth specification '%s', registry must not be blank", cred)
case len(credParts) == 2 && (len(userPart) == 0 || len(credPart) == 0):
return fmt.Errorf("invalid registry auth specification '%s', username and password must not be blank", cred)
case len(credParts) == 1 && len(userPart) == 0:
return fmt.Errorf("invalid registry auth specification '%s', token must not be blank", cred)
case len(credParts) == 2:
auth = spec.RegistryAuth{
Username: userPart,
Password: credPart,
}
case len(credParts) == 1:
auth = spec.RegistryAuth{
RegistryToken: authPart,
}
case !push:
msg = fmt.Sprintf("Building %q", p.Tag())
action = "building"
case nobuild:
msg = fmt.Sprintf("Pushing %q without building", p.Tag())
action = "building and pushing"
default:
return fmt.Errorf("invalid registry auth specification '%s'", cred)
msg = fmt.Sprintf("Building and pushing %q", p.Tag())
action = "building and pushing"
}
registryCredMap[registryPart] = auth
}
opts = append(opts, pkglib.WithRegistryAuth(registryCredMap))
}
for _, p := range pkgs {
// things we need our own copies of
var (
pkgOpts = make([]pkglib.BuildOpt, len(opts))
pkgPlats = make([]imagespec.Platform, len(plats))
)
copy(pkgOpts, opts)
copy(pkgPlats, plats)
// unless overridden, platforms are specific to a package, so this needs to be inside the for loop
if len(pkgPlats) == 0 {
for _, a := range p.Arches() {
if _, ok := skipPlatformsMap[a]; ok {
continue
}
pkgPlats = append(pkgPlats, imagespec.Platform{OS: "linux", Architecture: a})
fmt.Println(msg)
if err := p.Build(pkgOpts...); err != nil {
return fmt.Errorf("error %s %q: %w", action, p.Tag(), err)
}
}
// if there are no platforms to build for, do nothing.
// note that this is *not* an error; we simply skip it
if len(pkgPlats) == 0 {
fmt.Printf("Skipping %s with no architectures to build\n", p.Tag())
continue
}
pkgOpts = append(pkgOpts, pkglib.WithBuildPlatforms(pkgPlats...))
var msg, action string
switch {
case !withPush:
msg = fmt.Sprintf("Building %q", p.Tag())
action = "building"
case nobuild:
msg = fmt.Sprintf("Pushing %q without building", p.Tag())
action = "building and pushing"
default:
msg = fmt.Sprintf("Building and pushing %q", p.Tag())
action = "building and pushing"
}
fmt.Println(msg)
if err := p.Build(pkgOpts...); err != nil {
return fmt.Errorf("error %s %q: %w", action, p.Tag(), err)
}
}
return nil
return nil
},
}
cmd.Flags().BoolVar(&force, "force", false, "Force rebuild even if image is in local cache")
cmd.Flags().BoolVar(&pull, "pull", false, "Pull image if in registry but not in local cache; conflicts with --force")
cmd.Flags().BoolVar(&push, "push", false, "After building, if successful, push the image to the registry; if --nobuild is set, just push")
cmd.Flags().BoolVar(&ignoreCache, "ignore-cached", false, "Ignore cached intermediate images, always pulling from registry")
cmd.Flags().BoolVar(&docker, "docker", false, "Store the built image in the docker image cache instead of the default linuxkit cache")
cmd.Flags().StringVar(&platforms, "platforms", "", "Which platforms to build for, defaults to all of those for which the package can be built")
@ -287,18 +295,6 @@ func addCmdRunPkgBuildPush(cmd *cobra.Command, withPush bool) *cobra.Command {
return cmd
}
func pkgBuildCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "build",
Short: "build an OCI package from a directory with a yaml configuration file",
Long: `Build an OCI package from a directory with a yaml configuration file.
'path' specifies the path to the package source directory.
`,
Example: ` linuxkit pkg build [options] pkg/dir/`,
Args: cobra.MinimumNArgs(1),
}
return addCmdRunPkgBuildPush(cmd, false)
}
func buildPlatformBuildersMap(inputs string, existing map[string]string) (map[string]string, error) {
if inputs == "" {

View File

@ -1,18 +1,35 @@
package main
import "github.com/spf13/cobra"
import (
"fmt"
func pkgPushCmd() *cobra.Command {
"github.com/spf13/cobra"
)
func pkgPushCmd(buildCmd *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "push",
Short: "build and push an OCI package from a directory with a yaml configuration file",
Short: "Alias for 'pkg build --push'",
Long: `Build and push an OCI package from a directory with a yaml configuration file.
'path' specifies the path to the package source directory.
The package may or may not be built first, depending on options
`,
Example: ` linuxkit pkg push [options] pkg/dir/`,
Args: cobra.MinimumNArgs(1),
Example: ` linuxkit pkg push [options] pkg/dir/`,
SuggestFor: []string{"build"},
Args: cobra.MinimumNArgs(1),
Deprecated: "use 'pkg build --push' instead",
RunE: func(cmd *cobra.Command, args []string) error {
// Create a copy of buildCmd with push=true
if err := buildCmd.Flags().Set("push", "true"); err != nil {
return fmt.Errorf("'pkg push' unable to set 'pkg build --push': %w", err)
}
// Pass the args to the build command
buildCmd.SetArgs(args)
return buildCmd.RunE(buildCmd, args)
},
}
return addCmdRunPkgBuildPush(cmd, true)
cmd.Flags().AddFlagSet(buildCmd.Flags())
return cmd
}