github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow@v0.28.1-0.20240311201729-34c6856b157f/pkg/command/quarkus/build.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one
     3   * or more contributor license agreements.  See the NOTICE file
     4   * distributed with this work for additional information
     5   * regarding copyright ownership.  The ASF licenses this file
     6   * to you under the Apache License, Version 2.0 (the
     7   * "License"); you may not use this file except in compliance
     8   * with the License.  You may obtain a copy of the License at
     9   *
    10   *  http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing,
    13   * software distributed under the License is distributed on an
    14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    15   * KIND, either express or implied.  See the License for the
    16   * specific language governing permissions and limitations
    17   * under the License.
    18   */
    19  
    20  package quarkus
    21  
    22  import (
    23  	"fmt"
    24  	"github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/common"
    25  	"github.com/apache/incubator-kie-tools/packages/kn-plugin-workflow/pkg/metadata"
    26  	"github.com/ory/viper"
    27  	"github.com/spf13/cobra"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  )
    32  
    33  type BuildCmdConfig struct {
    34  	Image      string // full image name
    35  	Registry   string // image registry (overrides image name)
    36  	Repository string // image repository (overrides image name)
    37  	ImageName  string // image name (overrides image name)
    38  	Tag        string // image tag (overrides image name)
    39  
    40  	// Build strategy options
    41  	Jib       bool // use Jib extension to build the image and push it to a remote registry
    42  	JibPodman bool // use Jib extension to build the image and save it in podman
    43  	Push      bool // choose to push an image to a remote registry or not (Docker only)
    44  
    45  	Test bool // choose to run the project tests
    46  }
    47  
    48  func NewBuildCommand() *cobra.Command {
    49  	var cmd = &cobra.Command{
    50  		Use:   "build",
    51  		Short: "Build a Quarkus SonataFlow project and generate a container image",
    52  		Long: `
    53  	Builds a Quarkus SonataFlow project in the current directory 
    54  	resulting in a container image.  
    55  	By default the resultant container image will have the project name. It can be 
    56  	overridden with the --image or with others image options, see help for more information.
    57  
    58  	During the build, a knative.yml file will be generated on the ./target/kubernetes folder.
    59  	If your workflow uses eventing, an additional kogito.yml is also generated.
    60  	The deploy command uses both these files.
    61  
    62  	Authentication is required if you want to push the resultant image to a private registry.
    63  	To authenticate to your registry, use "docker login" or any other equivalent method.
    64  `,
    65  		Example: `
    66  	# Build from the local directory
    67  	# The full image name will be determined automatically based on the
    68  	# project's directory name
    69  	{{.Name}} build
    70  	
    71  	# Build from the local directory, specifying the full image name
    72  	{{.Name}} build --image quay.io/myuser/myworkflow:1.0
    73  	
    74  	# Build from the local directory, specifying the full image name and pushing
    75  	# it to the remote registry (authentication can be necessary, use docker login)
    76  	# NOTE: If no registry is specfied in the image full name, quay.io will be used.
    77  	{{.Name}} build --image quay.io/mysuer/myworkflow:1.0 --push
    78  	
    79  	# Build from the local directory, passing separately image options
    80  	{{.Name}} build --image-registry docker.io --image-repository myuser --image-name myworkflow --image-tag 1.0
    81  
    82  	# Build using Jib instead of Docker. (Read more: https://kiegroup.github.io/kogito-docs/serverlessworkflow/main/cloud/build-workflow-image-with-quarkus-cli.html)
    83  	# Docker is still required to save the image if the push flag is not used
    84  	{{.Name}} build --jib
    85  	
    86  	# Build using Jib and save the image in podman
    87  	# Can't use the "push" or "jib" flag for this build strategy
    88  	{{.Name}} build --jib-podman
    89  	`,
    90  		SuggestFor: []string{"biuld", "buidl", "built"},
    91  		PreRunE:    common.BindEnv("image", "image-registry", "image-repository", "image-name", "image-tag", "jib", "jib-podman", "push", "test"),
    92  	}
    93  
    94  	cmd.RunE = func(cmd *cobra.Command, args []string) error {
    95  		_, err := runBuild(cmd)
    96  		return err
    97  	}
    98  
    99  	cmd.Flags().StringP("image", "i", "", "Full image name in the form of [registry]/[repository]/[name]:[tag]")
   100  	cmd.Flags().String("image-registry", "", "Image registry, ex: quay.io, if the --image flag is in use this option overrides image [registry]")
   101  	cmd.Flags().String("image-repository", "", "Image repository, ex: registry-user or registry-project, if the --image flag is in use, this option overrides image [repository]")
   102  	cmd.Flags().String("image-name", "", "Image name, ex: new-project, if the --image flag is in use, this option overrides the image [name]")
   103  	cmd.Flags().String("image-tag", "", "Image tag, ex: 1.0, if the --image flag is in use, this option overrides the image [tag]")
   104  
   105  	cmd.Flags().Bool("jib", false, "Use Jib extension to generate the image (Docker is still required to save the generated image if push is not used)")
   106  	cmd.Flags().Bool("jib-podman", false, "Use Jib extension to generate the image and save it in podman (can't use --push)")
   107  	cmd.Flags().Bool("push", false, "Attempt to push the genereated image after being successfully built")
   108  
   109  	cmd.Flags().Bool("test", false, "Run the project tests")
   110  
   111  	cmd.SetHelpFunc(common.DefaultTemplatedHelp)
   112  	return cmd
   113  }
   114  
   115  func runBuild(cmd *cobra.Command) (out string, err error) {
   116  	fmt.Println("🔨 Building your Quarkus SonataFlow project...")
   117  
   118  	cfg, err := runBuildCmdConfig(cmd)
   119  	if err != nil {
   120  		err = fmt.Errorf("initializing build config: %w", err)
   121  		return
   122  	}
   123  
   124  	if err = common.CheckJavaDependencies(); err != nil {
   125  		return
   126  	}
   127  
   128  	if cfg.JibPodman {
   129  		if err = common.CheckPodman(); err != nil {
   130  			return
   131  		}
   132  	} else if (cfg.Jib && !cfg.Push) || (!cfg.Jib) {
   133  		if err = common.CheckDocker(); err != nil {
   134  			return
   135  		}
   136  	}
   137  
   138  	if err != nil {
   139  		return
   140  	}
   141  
   142  	if err = runAddExtension(cfg); err != nil {
   143  		return
   144  	}
   145  
   146  	if out, err = runBuildImage(cfg); err != nil {
   147  		return
   148  	}
   149  
   150  	fmt.Println("✅ Quarkus SonataFlow project successfully built")
   151  
   152  	return
   153  }
   154  
   155  func runBuildCmdConfig(cmd *cobra.Command) (cfg BuildCmdConfig, err error) {
   156  	cfg = BuildCmdConfig{
   157  		Image:      viper.GetString("image"),
   158  		Registry:   viper.GetString("image-registry"),
   159  		Repository: viper.GetString("image-repository"),
   160  		ImageName:  viper.GetString("image-name"),
   161  		Tag:        viper.GetString("image-tag"),
   162  
   163  		Jib:       viper.GetBool("jib"),
   164  		JibPodman: viper.GetBool("jib-podman"),
   165  		Push:      viper.GetBool("push"),
   166  
   167  		Test: viper.GetBool("test"),
   168  	}
   169  	if len(cfg.Image) == 0 && len(cfg.ImageName) == 0 {
   170  		fmt.Println("ERROR: either --image or --image-name should be used")
   171  		err = fmt.Errorf("missing flags")
   172  	}
   173  
   174  	if cfg.JibPodman && cfg.Push {
   175  		fmt.Println("ERROR: can't use --jib-podman with --push")
   176  		err = fmt.Errorf("invalid flags")
   177  	}
   178  
   179  	if cfg.JibPodman && cfg.Jib {
   180  		fmt.Println("ERROR: can't use --jib-podman with --jib")
   181  		err = fmt.Errorf("invalid flags")
   182  	}
   183  
   184  	return
   185  }
   186  
   187  // This function removes the extension that is not going to be used (if present)
   188  // and updates the chosen one. The entire operation is handled as an extension addition.
   189  // Therefore the removal is hidden from the user
   190  func runAddExtension(cfg BuildCmdConfig) error {
   191  	if cfg.Jib || cfg.JibPodman {
   192  		fmt.Printf(" - Adding Quarkus Jib extension\n")
   193  		if err := common.RunExtensionCommand(
   194  			"quarkus:remove-extension",
   195  			metadata.QuarkusContainerImageDocker,
   196  		); err != nil {
   197  			return err
   198  		}
   199  		if err := common.RunExtensionCommand(
   200  			"quarkus:add-extension",
   201  			metadata.QuarkusContainerImageJib,
   202  		); err != nil {
   203  			return err
   204  		}
   205  	} else {
   206  		fmt.Printf(" - Adding Quarkus Docker extension\n")
   207  		if err := common.RunExtensionCommand(
   208  			"quarkus:remove-extension",
   209  			metadata.QuarkusContainerImageJib,
   210  		); err != nil {
   211  			return err
   212  		}
   213  		if err := common.RunExtensionCommand(
   214  			"quarkus:add-extension",
   215  			metadata.QuarkusContainerImageDocker,
   216  		); err != nil {
   217  			return err
   218  		}
   219  	}
   220  
   221  	fmt.Println("✅ Quarkus extension was successfully added to the project pom.xml")
   222  	return nil
   223  }
   224  
   225  func runBuildImage(cfg BuildCmdConfig) (out string, err error) {
   226  	registry, repository, name, tag := getImageConfig(cfg)
   227  	if err = checkImageName(name); err != nil {
   228  		return
   229  	}
   230  
   231  	skipTestsConfig := getSkipTestsConfig(cfg)
   232  	builderConfig := getBuilderConfig(cfg)
   233  	executableName := getExecutableNameConfig(cfg)
   234  
   235  	build := common.ExecCommand("mvn", "package",
   236  		skipTestsConfig,
   237  		"-Dquarkus.kubernetes.deployment-target=knative",
   238  		fmt.Sprintf("-Dquarkus.knative.name=%s", name),
   239  		"-Dquarkus.container-image.build=true",
   240  		fmt.Sprintf("-Dquarkus.container-image.registry=%s", registry),
   241  		fmt.Sprintf("-Dquarkus.container-image.group=%s", repository),
   242  		fmt.Sprintf("-Dquarkus.container-image.name=%s", name),
   243  		fmt.Sprintf("-Dquarkus.container-image.tag=%s", tag),
   244  		fmt.Sprintf("-Dquarkus.container-image.push=%s", strconv.FormatBool(cfg.Push)),
   245  		builderConfig,
   246  		executableName,
   247  	)
   248  
   249  	if err = common.RunCommand(
   250  		build,
   251  		"build",
   252  	); err != nil {
   253  		if cfg.Push {
   254  			fmt.Println("ERROR: Image build failed.")
   255  			fmt.Println("If you're using a private registry, check if you're authenticated")
   256  		}
   257  		return
   258  	}
   259  
   260  	out = getImage(registry, repository, name, tag)
   261  	if cfg.Push {
   262  		fmt.Printf("Created and pushed an image to registry: %s\n", out)
   263  	} else {
   264  		fmt.Printf("Created a local image: %s\n", out)
   265  	}
   266  
   267  	fmt.Println("✅ Build success")
   268  	return
   269  }
   270  
   271  func checkImageName(name string) (err error) {
   272  	matched, err := regexp.MatchString("^[a-z]([-a-z0-9]*[a-z0-9])?$", name)
   273  	if !matched {
   274  		fmt.Println(`
   275  ERROR: Image name should match [a-z]([-a-z0-9]*[a-z0-9])?
   276  The name needs to start with a lower case letter and then it can be composed exclusvely of lower case letters, numbers or dashes ('-')
   277  Example of valid names: "test-0-0-1", "test", "t1"
   278  Example of invalid names: "1-test", "test.1", "test/1"
   279  		`)
   280  		err = fmt.Errorf("invalid image name")
   281  	}
   282  	return
   283  }
   284  
   285  // Use the --image-registry, --image-repository, --image-name, --image-tag to override the --image flag
   286  func getImageConfig(cfg BuildCmdConfig) (string, string, string, string) {
   287  	imageTagArray := strings.Split(cfg.Image, ":")
   288  	imageArray := strings.SplitN(imageTagArray[0], "/", 3)
   289  
   290  	var registry = ""
   291  	if len(cfg.Registry) > 0 {
   292  		registry = cfg.Registry
   293  	} else if len(imageArray) > 1 {
   294  		registry = imageArray[0]
   295  	}
   296  
   297  	var repository = ""
   298  	if len(cfg.Repository) > 0 {
   299  		repository = cfg.Repository
   300  	} else if len(imageArray) == 3 {
   301  		repository = imageArray[1]
   302  	}
   303  
   304  	var name = ""
   305  	if len(cfg.ImageName) > 0 {
   306  		name = cfg.ImageName
   307  	} else if len(imageArray) == 1 {
   308  		name = imageArray[0]
   309  	} else if len(imageArray) == 2 {
   310  		name = imageArray[1]
   311  	} else if len(imageArray) == 3 {
   312  		name = imageArray[2]
   313  	}
   314  
   315  	var tag = metadata.DefaultTag
   316  	if len(cfg.Tag) > 0 {
   317  		tag = cfg.Tag
   318  	} else if len(imageTagArray) > 1 && len(imageTagArray[1]) > 0 {
   319  		tag = imageTagArray[1]
   320  	}
   321  
   322  	return registry, repository, name, tag
   323  }
   324  
   325  func getImage(registry string, repository string, name string, tag string) string {
   326  	if len(registry) == 0 && len(repository) == 0 {
   327  		return fmt.Sprintf("%s:%s", name, tag)
   328  	} else if len(registry) == 0 {
   329  		return fmt.Sprintf("%s/%s:%s", repository, name, tag)
   330  	} else if len(repository) == 0 {
   331  		return fmt.Sprintf("%s/%s:%s", registry, name, tag)
   332  	}
   333  	return fmt.Sprintf("%s/%s/%s:%s", registry, repository, name, tag)
   334  }
   335  
   336  func getSkipTestsConfig(cfg BuildCmdConfig) string {
   337  	skipTests := "-DskipTests="
   338  	if cfg.Test {
   339  		skipTests += "false"
   340  	} else {
   341  		skipTests += "true"
   342  	}
   343  	return skipTests
   344  }
   345  
   346  func getBuilderConfig(cfg BuildCmdConfig) string {
   347  	builder := "-Dquarkus.container-image.builder="
   348  	if cfg.Jib || cfg.JibPodman {
   349  		builder += "jib"
   350  	} else {
   351  		builder += "docker"
   352  	}
   353  	return builder
   354  }
   355  
   356  func getExecutableNameConfig(cfg BuildCmdConfig) string {
   357  	executableName := "-Dquarkus.jib.docker-executable-name="
   358  	if cfg.JibPodman {
   359  		executableName += "podman"
   360  	} else {
   361  		executableName += "docker"
   362  	}
   363  	return executableName
   364  }