2024-07-21 22:15:11 +02:00
package common
import (
2024-07-26 16:53:09 +02:00
"bytes"
2024-07-21 22:15:11 +02:00
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"net/url"
2024-08-01 18:10:45 +02:00
"slices"
2024-07-21 22:15:11 +02:00
)
type ObsClient struct {
baseUrl * url . URL
client * http . Client
user , password string
cookie string
2024-07-22 17:35:48 +02:00
HomeProject string
2024-07-21 22:15:11 +02:00
}
2024-07-22 17:09:45 +02:00
func NewObsClient ( host string ) ( * ObsClient , error ) {
2024-07-21 22:15:11 +02:00
baseUrl , err := url . Parse ( "https://" + host )
if err != nil {
return nil , err
}
return & ObsClient {
baseUrl : baseUrl ,
client : & http . Client { } ,
2024-07-22 17:09:45 +02:00
user : obsUser ,
password : obsPassword ,
2024-07-22 17:35:48 +02:00
HomeProject : fmt . Sprintf ( "home:%s" , obsUser ) ,
2024-07-21 22:15:11 +02:00
} , nil
}
2024-07-26 16:53:09 +02:00
type RepositoryPathMeta struct {
Project string ` xml:"project,attr" `
Repository string ` xml:"repository,attr" `
}
2024-07-21 22:15:11 +02:00
type RepositoryMeta struct {
2024-08-19 13:54:44 +02:00
Name string ` xml:"name,attr" `
BuildTrigger string ` xml:"rebuild,attr" `
BlockMode string ` xml:"block" `
LinkedBuild string ` xml:"linkedbuild" `
Archs [ ] string ` xml:"arch" `
Paths [ ] RepositoryPathMeta ` xml:"path" `
2024-07-21 22:15:11 +02:00
}
2024-07-22 17:09:45 +02:00
type Flags struct {
Contents string ` xml:",innerxml" `
}
2024-07-21 22:15:11 +02:00
type ProjectMeta struct {
2024-07-26 16:53:09 +02:00
XMLName xml . Name ` xml:"project" `
Name string ` xml:"name,attr" `
Title string ` xml:"title" `
Description string ` xml:"description" `
2024-08-01 12:36:23 +02:00
Url string ` xml:"url" `
2024-07-26 16:53:09 +02:00
ScmSync string ` xml:"scmsync" `
Repositories [ ] RepositoryMeta ` xml:"repository" `
2024-07-22 17:09:45 +02:00
BuildFlags Flags ` xml:"build" `
PublicFlags Flags ` xml:"publish" `
2024-07-26 16:53:09 +02:00
DebugFlags Flags ` xml:"debuginfo" `
UseForBuild Flags ` xml:"useforbuild" `
2024-07-21 22:15:11 +02:00
}
func parseProjectMeta ( data [ ] byte ) ( * ProjectMeta , error ) {
var meta ProjectMeta
err := xml . Unmarshal ( data , & meta )
if err != nil {
return nil , err
}
return & meta , nil
}
func ( c * ObsClient ) GetProjectMeta ( project string ) ( * ProjectMeta , error ) {
req , err := http . NewRequest ( "GET" , c . baseUrl . JoinPath ( "source" , project , "_meta" ) . String ( ) , nil )
if err != nil {
return nil , err
}
req . SetBasicAuth ( c . user , c . password )
2024-07-26 16:53:09 +02:00
log . Printf ( "request: %#v" , * req . URL )
log . Printf ( "headers: %#v" , req . Header )
2024-07-21 22:15:11 +02:00
res , err := c . client . Do ( req )
if err != nil {
return nil , err
}
switch res . StatusCode {
case 200 :
break
case 404 :
return nil , nil
default :
return nil , fmt . Errorf ( "Unexpected return code: %d" , res . StatusCode )
}
data , err := io . ReadAll ( res . Body )
if err != nil {
return nil , err
}
return parseProjectMeta ( data )
}
2024-07-26 16:53:09 +02:00
func ObsSafeProjectName ( prjname string ) string {
if len ( prjname ) < 1 {
return prjname
} else if len ( prjname ) > 200 {
prjname = prjname [ : 199 ]
}
switch prjname [ 0 ] {
case '_' , '.' , ':' :
prjname = "X" + prjname [ 1 : ]
2024-08-01 12:36:23 +02:00
// no UTF-8 in OBS :(
// prjname = "_ " + prjname[1:]
// case ':':
// prjname = ": " + prjname[1:]
// case '.':
// prjname = "․ " + prjname[1:]
2024-07-26 16:53:09 +02:00
}
return prjname
}
2024-08-19 13:54:44 +02:00
var ValidBlockModes [ ] string
var ValidPrjLinkModes [ ] string
var ValidTriggerModes [ ] string
2024-08-01 12:36:23 +02:00
func ( c * ObsClient ) SetProjectMeta ( meta * ProjectMeta ) error {
2024-07-26 16:53:09 +02:00
2024-08-19 13:54:44 +02:00
for _ , repo := range meta . Repositories {
if len ( repo . BlockMode ) > 0 && ! slices . Contains ( ValidBlockModes , repo . BlockMode ) {
return fmt . Errorf ( "Invalid repository block mode: '%s'" , repo . BlockMode )
}
if len ( repo . BuildTrigger ) > 0 && ! slices . Contains ( ValidTriggerModes , repo . BuildTrigger ) {
return fmt . Errorf ( "Invalid repository trigger mode: '%s'" , repo . BuildTrigger )
}
if len ( repo . LinkedBuild ) > 0 && ! slices . Contains ( ValidPrjLinkModes , repo . LinkedBuild ) {
return fmt . Errorf ( "Invalid linked project rebuild mode: '%s'" , repo . LinkedBuild )
}
}
2024-07-26 16:53:09 +02:00
req , err := http . NewRequest ( "PUT" , c . baseUrl . JoinPath ( "source" , meta . Name , "_meta" ) . String ( ) , nil )
if err != nil {
return err
}
req . SetBasicAuth ( c . user , c . password )
xml , err := xml . Marshal ( meta )
if err != nil {
return err
}
req . Body = io . NopCloser ( bytes . NewReader ( xml ) )
log . Printf ( "headers: %#v" , req . Header )
log . Printf ( "xml: %s" , xml )
res , err := c . client . Do ( req )
if err != nil {
return err
}
switch res . StatusCode {
case 200 :
break
default :
return fmt . Errorf ( "Unexpected return code: %d" , res . StatusCode )
}
return nil
}
2024-07-21 22:15:11 +02:00
func ( c * ObsClient ) DeleteProject ( project string ) error {
req , err := http . NewRequest ( "DELETE" , c . baseUrl . JoinPath ( "source" , project ) . String ( ) , nil )
if err != nil {
return err
}
req . SetBasicAuth ( c . user , c . password )
res , err := c . client . Do ( req )
if err != nil {
return err
}
if res . StatusCode != 200 {
return fmt . Errorf ( "Unexpected return code: %d" , res . StatusCode )
}
return nil
}
2024-08-01 18:10:45 +02:00
type PackageBuildStatus struct {
2024-07-21 22:15:11 +02:00
Package string ` xml:"package,attr" `
Code string ` xml:"code,attr" `
Details string ` xml:"details" `
}
type BuildResult struct {
2024-08-01 18:10:45 +02:00
Project string ` xml:"project,attr" `
Repository string ` xml:"repository,attr" `
Arch string ` xml:"arch,attr" `
Code string ` xml:"code,attr" `
Dirty bool ` xml:"dirty,attr" `
Status [ ] PackageBuildStatus ` xml:"status" `
Binaries [ ] BinaryList ` xml:"binarylist" `
2024-07-21 22:15:11 +02:00
}
type Binary struct {
Size uint64 ` xml:"size,attr" `
Filename string ` xml:"filename,attr" `
Mtime uint64 ` xml:"mtime,attr" `
}
type BinaryList struct {
Package string ` xml:"package,attr" `
Binary [ ] Binary ` xml:"binary" `
}
type BuildResultList struct {
XMLName xml . Name ` xml:"resultlist" `
Result [ ] BuildResult ` xml:"result" `
}
2024-08-01 18:10:45 +02:00
func ( r * BuildResultList ) GetPackageList ( ) [ ] string {
pkgList := make ( [ ] string , 0 , 16 )
for _ , res := range r . Result {
// TODO: enough to iterate over one result set?
for _ , status := range res . Status {
if ! slices . Contains ( pkgList , status . Package ) {
pkgList = append ( pkgList , status . Package )
}
}
}
return pkgList
}
func ( r * BuildResultList ) BuildResultSummary ( ) ( success , finished bool ) {
2024-08-02 15:34:44 +02:00
if r == nil {
return true , true
}
2024-08-01 18:10:45 +02:00
finished = len ( r . Result ) > 0 && len ( r . Result [ 0 ] . Status ) > 0
success = finished
for _ , resultSet := range r . Result {
repoDetail , ok := ObsRepoStatusDetails [ resultSet . Code ]
if ! ok {
panic ( "Unknown repo result code: " + resultSet . Code )
}
finished = repoDetail . Finished
if ! finished || resultSet . Dirty {
return
}
for _ , result := range resultSet . Status {
detail , ok := ObsBuildStatusDetails [ result . Code ]
if ! ok {
panic ( "Unknown result code: " + result . Code )
}
finished = finished && detail . Finished
success = success && detail . Success
if ! finished {
return
}
}
}
return
}
var ObsBuildStatusDetails map [ string ] ObsBuildStatusDetail
var ObsRepoStatusDetails map [ string ] ObsBuildStatusDetail
type ObsBuildStatusDetail struct {
Code string
Description string
Finished bool
Success bool
}
func init ( ) {
2024-08-19 13:54:44 +02:00
ValidTriggerModes = [ ] string { "transitive" , "direct" , "local" }
ValidBlockModes = [ ] string { "all" , "local" , "never" }
ValidPrjLinkModes = [ ] string { "off" , "localdep" , "alldirect" , "all" }
2024-08-01 18:10:45 +02:00
ObsBuildStatusDetails = make ( map [ string ] ObsBuildStatusDetail )
ObsRepoStatusDetails = make ( map [ string ] ObsBuildStatusDetail )
// package status
ObsBuildStatusDetails [ "succeeded" ] = ObsBuildStatusDetail {
Code : "succeeded" ,
Description : "Package has built successfully and can be used to build further packages." ,
Finished : true ,
Success : true ,
}
ObsBuildStatusDetails [ "failed" ] = ObsBuildStatusDetail {
Code : "failed" ,
Description : "The package does not build successfully. No packages have been created. Packages that depend on this package will be built using any previously created packages, if they exist." ,
Finished : true ,
Success : false ,
}
ObsBuildStatusDetails [ "unresolvable" ] = ObsBuildStatusDetail {
Code : "unresolvable" ,
Description : "The build can not begin, because required packages are either missing or not explicitly defined." ,
Finished : true ,
Success : false ,
}
ObsBuildStatusDetails [ "broken" ] = ObsBuildStatusDetail {
Code : "broken" ,
Description : "The sources either contain no build description (e.g. specfile), automatic source processing failed or a merge conflict does exist." ,
Finished : true ,
Success : false ,
}
ObsBuildStatusDetails [ "blocked" ] = ObsBuildStatusDetail {
Code : "blocked" ,
Description : "This package waits for other packages to be built. These can be in the same or other projects." ,
Finished : false ,
}
ObsBuildStatusDetails [ "scheduled" ] = ObsBuildStatusDetail {
Code : "scheduled" ,
Description : "A package has been marked for building, but the build has not started yet." ,
Finished : false ,
}
ObsBuildStatusDetails [ "dispatching" ] = ObsBuildStatusDetail {
Code : "dispatching" ,
Description : "A package is being copied to a build host. This is an intermediate state before building." ,
Finished : false ,
}
ObsBuildStatusDetails [ "building" ] = ObsBuildStatusDetail {
Code : "building" ,
Description : "The package is currently being built." ,
Finished : false ,
}
ObsBuildStatusDetails [ "signing" ] = ObsBuildStatusDetail {
Code : "signing" ,
Description : "The package has been built successfully and is assigned to get signed." ,
Finished : false ,
}
ObsBuildStatusDetails [ "finished" ] = ObsBuildStatusDetail {
Code : "finished" ,
Description : "The package has been built and signed, but has not yet been picked up by the scheduler. This is an intermediate state prior to 'succeeded' or 'failed'." ,
Finished : false ,
}
ObsBuildStatusDetails [ "disabled" ] = ObsBuildStatusDetail {
Code : "disabled" ,
Description : "The package has been disabled from building in project or package metadata. Packages that depend on this package will be built using any previously created packages, if they still exist." ,
Finished : true ,
Success : true ,
}
ObsBuildStatusDetails [ "excluded" ] = ObsBuildStatusDetail {
Code : "excluded" ,
Description : "The package build has been disabled in package build description (for example in the .spec file) or does not provide a matching build description for the target." ,
Finished : true ,
Success : true ,
}
ObsBuildStatusDetails [ "locked" ] = ObsBuildStatusDetail {
Code : "locked" ,
Description : "The package is frozen" ,
Finished : true ,
Success : true ,
}
ObsBuildStatusDetails [ "unknown" ] = ObsBuildStatusDetail {
Code : "unknown" ,
Description : "The scheduler has not yet evaluated this package. Should be a short intermediate state for new packages." ,
Finished : false ,
}
// repo status
ObsRepoStatusDetails [ "published" ] = ObsBuildStatusDetail {
Code : "published" ,
Description : "Repository has been published" ,
Finished : true ,
}
ObsRepoStatusDetails [ "publishing" ] = ObsBuildStatusDetail {
Code : "publishing" ,
Description : "Repository is being created right now" ,
Finished : true ,
}
ObsRepoStatusDetails [ "unpublished" ] = ObsBuildStatusDetail {
Code : "unpublished" ,
Description : "Build finished, but repository publishing is disabled" ,
Finished : true ,
}
ObsRepoStatusDetails [ "building" ] = ObsBuildStatusDetail {
Code : "building" ,
Description : "Build jobs exist for the repository" ,
Finished : false ,
}
ObsRepoStatusDetails [ "finished" ] = ObsBuildStatusDetail {
Code : "finished" ,
Description : "Build jobs have been processed, new repository is not yet created" ,
Finished : true ,
}
ObsRepoStatusDetails [ "blocked" ] = ObsBuildStatusDetail {
Code : "blocked" ,
Description : "No build possible at the moment, waiting for jobs in other repositories" ,
Finished : false ,
}
ObsRepoStatusDetails [ "broken" ] = ObsBuildStatusDetail {
Code : "broken" ,
Description : "The repository setup is broken, build or publish not possible" ,
Finished : true ,
}
ObsRepoStatusDetails [ "scheduling" ] = ObsBuildStatusDetail {
Code : "scheduling" ,
Description : "The repository state is being calculated right now" ,
Finished : false ,
}
}
2024-07-21 22:15:11 +02:00
func parseBuildResults ( data [ ] byte ) ( * BuildResultList , error ) {
result := BuildResultList { }
err := xml . Unmarshal ( data , & result )
if err != nil {
return nil , err
}
return & result , nil
}
2024-08-01 12:36:23 +02:00
type ObsProjectNotFound struct {
Project string
}
func ( obs ObsProjectNotFound ) Error ( ) string {
return fmt . Sprintf ( "OBS project is not found: %s" , obs . Project )
}
2024-07-31 16:52:02 +02:00
func ( c * ObsClient ) BuildStatus ( project string , packages ... string ) ( * BuildResultList , error ) {
2024-07-21 22:15:11 +02:00
u := c . baseUrl . JoinPath ( "build" , project , "_result" )
query := u . Query ( )
query . Add ( "view" , "status" )
query . Add ( "view" , "binarylist" )
query . Add ( "multibuild" , "1" )
2024-08-16 14:04:43 +02:00
if len ( packages ) > 0 {
query . Add ( "lastbuild" , "1" )
for _ , pkg := range packages {
query . Add ( "package" , pkg )
}
2024-07-31 16:52:02 +02:00
}
2024-07-21 22:15:11 +02:00
u . RawQuery = query . Encode ( )
req , err := http . NewRequest ( "GET" , u . String ( ) , nil )
log . Print ( u . String ( ) )
if err != nil {
return nil , err
}
req . SetBasicAuth ( c . user , c . password )
res , err := c . client . Do ( req )
if err != nil {
return nil , err
}
2024-08-01 12:36:23 +02:00
switch res . StatusCode {
case 200 :
break
case 404 :
return nil , ObsProjectNotFound { project }
default :
2024-07-21 22:15:11 +02:00
return nil , fmt . Errorf ( "Unexpected return code: %d" , res . StatusCode )
}
data , err := io . ReadAll ( res . Body )
if err != nil {
return nil , err
}
return parseBuildResults ( data )
}