github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/oc/startbuild.go (about)

     1  package oc
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"errors"
     7  	"github.com/jfrog/gofrog/version"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"strings"
    12  
    13  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/container"
    14  
    15  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    16  	"github.com/jfrog/jfrog-cli-core/v2/common/build"
    17  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    18  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    19  	"github.com/jfrog/jfrog-client-go/utils/log"
    20  )
    21  
    22  const minSupportedOcVersion = "3.0.0"
    23  
    24  type OcStartBuildCommand struct {
    25  	executablePath     string
    26  	serverId           string
    27  	repo               string
    28  	ocArgs             []string
    29  	serverDetails      *config.ServerDetails
    30  	buildConfiguration *build.BuildConfiguration
    31  }
    32  
    33  func NewOcStartBuildCommand() *OcStartBuildCommand {
    34  	return &OcStartBuildCommand{}
    35  }
    36  
    37  func (osb *OcStartBuildCommand) Run() error {
    38  	log.Info("Running oc start-build...")
    39  	var err error
    40  	if err = osb.validateAllowedOptions(); err != nil {
    41  		return err
    42  	}
    43  
    44  	osb.serverDetails, err = config.GetSpecificConfig(osb.serverId, true, true)
    45  	if err != nil {
    46  		return err
    47  	}
    48  
    49  	if err = osb.setOcExecutable(); err != nil {
    50  		return err
    51  	}
    52  	if err = osb.validateOcVersion(); err != nil {
    53  		return err
    54  	}
    55  
    56  	// Run the build on OpenShift
    57  	ocBuildName, err := startBuild(osb.executablePath, osb.ocArgs)
    58  	if err != nil {
    59  		return err
    60  	}
    61  	log.Info("Build", ocBuildName, "finished successfully.")
    62  	buildName, err := osb.buildConfiguration.GetBuildName()
    63  	if err != nil {
    64  		return err
    65  	}
    66  	buildNumber, err := osb.buildConfiguration.GetBuildNumber()
    67  	if err != nil {
    68  		return err
    69  	}
    70  	project := osb.buildConfiguration.GetProject()
    71  	if buildName == "" {
    72  		return nil
    73  	}
    74  
    75  	log.Info("Collecting build info...")
    76  	// Get the new image details from OpenShift
    77  	imageTag, manifestSha256, err := getImageDetails(osb.executablePath, ocBuildName)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	if err := build.SaveBuildGeneralDetails(buildName, buildNumber, project); err != nil {
    83  		return err
    84  	}
    85  	serviceManager, err := utils.CreateServiceManager(osb.serverDetails, -1, 0, false)
    86  	if err != nil {
    87  		return err
    88  	}
    89  	image := container.NewImage(imageTag)
    90  	builder, err := container.NewRemoteAgentBuildInfoBuilder(image, osb.repo, buildName, buildNumber, project, serviceManager, manifestSha256)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	buildInfo, err := builder.Build(osb.buildConfiguration.GetModule())
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	log.Info("oc start-build finished successfully.")
   100  	return build.SaveBuildInfo(buildName, buildNumber, project, buildInfo)
   101  }
   102  
   103  func (osb *OcStartBuildCommand) ServerDetails() (*config.ServerDetails, error) {
   104  	return osb.serverDetails, nil
   105  }
   106  
   107  func (osb *OcStartBuildCommand) CommandName() string {
   108  	return "rt_oc_start_build"
   109  }
   110  
   111  func (osb *OcStartBuildCommand) setOcExecutable() error {
   112  	ocExecPath, err := exec.LookPath("oc")
   113  	if err != nil {
   114  		return errorutils.CheckError(err)
   115  	}
   116  
   117  	osb.executablePath = ocExecPath
   118  	log.Debug("Found OpenShift CLI executable at:", osb.executablePath)
   119  	return nil
   120  }
   121  
   122  func (osb *OcStartBuildCommand) SetOcArgs(args []string) *OcStartBuildCommand {
   123  	osb.ocArgs = args
   124  	return osb
   125  }
   126  
   127  func (osb *OcStartBuildCommand) SetRepo(repo string) *OcStartBuildCommand {
   128  	osb.repo = repo
   129  	return osb
   130  }
   131  
   132  func (osb *OcStartBuildCommand) SetServerId(serverId string) *OcStartBuildCommand {
   133  	osb.serverId = serverId
   134  	return osb
   135  }
   136  
   137  func (osb *OcStartBuildCommand) SetBuildConfiguration(buildConfiguration *build.BuildConfiguration) *OcStartBuildCommand {
   138  	osb.buildConfiguration = buildConfiguration
   139  	return osb
   140  }
   141  
   142  func (osb *OcStartBuildCommand) validateAllowedOptions() error {
   143  	notAllowedOcOptions := []string{"-w", "--wait", "--template", "-o", "--output"}
   144  	for _, arg := range osb.ocArgs {
   145  		for _, optionName := range notAllowedOcOptions {
   146  			if arg == optionName || strings.HasPrefix(arg, optionName+"=") {
   147  				return errorutils.CheckErrorf("the %s option is not allowed", optionName)
   148  			}
   149  		}
   150  	}
   151  	return nil
   152  }
   153  
   154  func (osb *OcStartBuildCommand) validateOcVersion() error {
   155  	ocVersionStr, err := getOcVersion(osb.executablePath)
   156  	if err != nil {
   157  		return err
   158  	}
   159  	trimmedVersion := strings.TrimPrefix(ocVersionStr, "v")
   160  	ocVersion := version.NewVersion(trimmedVersion)
   161  	if ocVersion.Compare(minSupportedOcVersion) > 0 {
   162  		return errorutils.CheckErrorf(
   163  			"JFrog CLI oc start-build command requires OpenShift CLI version " + minSupportedOcVersion + " or higher")
   164  	}
   165  	return nil
   166  }
   167  
   168  func startBuild(executablePath string, ocFlags []string) (ocBuildName string, err error) {
   169  	cmdArgs := []string{"start-build", "-w", "--template={{.metadata.name}}{{\"\\n\"}}"}
   170  	cmdArgs = append(cmdArgs, ocFlags...)
   171  
   172  	log.Debug("Running command: oc", strings.Join(cmdArgs, " "))
   173  	cmd := exec.Command(executablePath, cmdArgs...)
   174  	outputPr, outputPw, err := os.Pipe()
   175  	if err != nil {
   176  		return "", errorutils.CheckError(err)
   177  	}
   178  	outputWriter := io.MultiWriter(os.Stderr, outputPw)
   179  	cmd.Stdout = outputWriter
   180  	cmd.Stderr = os.Stderr
   181  	err = cmd.Start()
   182  	if err != nil {
   183  		return "", errorutils.CheckError(err)
   184  	}
   185  
   186  	// The build name is in the first line of the output
   187  	scanner := bufio.NewScanner(outputPr)
   188  	scanner.Scan()
   189  	ocBuildName = scanner.Text()
   190  
   191  	err = errorutils.CheckError(convertExitError(cmd.Wait()))
   192  	return
   193  }
   194  
   195  func getImageDetails(executablePath, ocBuildName string) (imageTag, manifestSha256 string, err error) {
   196  	cmdArgs := []string{"get", "build", ocBuildName, "--template={{.status.outputDockerImageReference}}@{{.status.output.to.imageDigest}}"}
   197  	log.Debug("Running command: oc", strings.Join(cmdArgs, " "))
   198  	outputBytes, err := exec.Command(executablePath, cmdArgs...).Output()
   199  	if err != nil {
   200  		return "", "", errorutils.CheckError(convertExitError(err))
   201  	}
   202  	output := string(outputBytes)
   203  	splitOutput := strings.Split(strings.TrimSpace(output), "@")
   204  	if len(splitOutput) != 2 {
   205  		return "", "", errorutils.CheckErrorf("Unable to parse image tag and digest of build %s. Output from OpenShift CLI: %s", ocBuildName, output)
   206  	}
   207  
   208  	return splitOutput[0], splitOutput[1], nil
   209  }
   210  
   211  func getOcVersion(executablePath string) (string, error) {
   212  	cmdArgs := []string{"version", "-o=json"}
   213  	outputBytes, err := exec.Command(executablePath, cmdArgs...).Output()
   214  	if err != nil {
   215  		// If an error occurred, maybe it's because the '-o' flag is not supported in OpenShift CLI v3 and below.
   216  		// Try executing this command without this flag.
   217  		return getOldOcVersion(executablePath)
   218  	}
   219  	var versionRes ocVersionResponse
   220  	err = json.Unmarshal(outputBytes, &versionRes)
   221  	if err != nil {
   222  		return "", errorutils.CheckError(err)
   223  	}
   224  
   225  	return versionRes.ClientVersion.GitVersion, nil
   226  }
   227  
   228  // Running 'oc version' without the '-o=json' flag that is not supported in OpenShift CLI v3 and below.
   229  func getOldOcVersion(executablePath string) (string, error) {
   230  	outputBytes, err := exec.Command(executablePath, "version").Output()
   231  	if err != nil {
   232  		return "", errorutils.CheckError(convertExitError(err))
   233  	}
   234  	// In OpenShift CLI v3 the output of 'oc version' looks like this:
   235  	// oc v3.0.0
   236  	// kubernetes v1.11.0
   237  	// [...]
   238  	// Get the first line of the output
   239  	scanner := bufio.NewScanner(strings.NewReader(string(outputBytes)))
   240  	scanner.Scan()
   241  	firstLine := scanner.Text()
   242  	if !strings.HasPrefix(firstLine, "oc v") {
   243  		return "", errorutils.CheckErrorf("Could not parse OpenShift CLI version. JFrog CLI oc start-build command requires OpenShift CLI version " + minSupportedOcVersion + " or higher.")
   244  	}
   245  	return strings.TrimPrefix(firstLine, "oc "), nil
   246  }
   247  
   248  func convertExitError(err error) error {
   249  	if exitError, ok := err.(*exec.ExitError); ok {
   250  		return errors.New(string(exitError.Stderr))
   251  	}
   252  	return err
   253  }
   254  
   255  type ocVersionResponse struct {
   256  	ClientVersion clientVersion `json:"clientVersion,omitempty"`
   257  }
   258  
   259  type clientVersion struct {
   260  	GitVersion string `json:"gitVersion,omitempty"`
   261  }