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 }