487 lines
13 KiB
Go
487 lines
13 KiB
Go
package common
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/xml"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"net/url"
|
||
"slices"
|
||
)
|
||
|
||
type ObsClient struct {
|
||
baseUrl *url.URL
|
||
client *http.Client
|
||
user, password string
|
||
cookie string
|
||
|
||
HomeProject string
|
||
}
|
||
|
||
func NewObsClient(host string) (*ObsClient, error) {
|
||
baseUrl, err := url.Parse("https://" + host)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &ObsClient{
|
||
baseUrl: baseUrl,
|
||
client: &http.Client{},
|
||
user: obsUser,
|
||
password: obsPassword,
|
||
|
||
HomeProject: fmt.Sprintf("home:%s", obsUser),
|
||
}, nil
|
||
}
|
||
|
||
type RepositoryPathMeta struct {
|
||
Project string `xml:"project,attr"`
|
||
Repository string `xml:"repository,attr"`
|
||
}
|
||
|
||
type RepositoryMeta struct {
|
||
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"`
|
||
}
|
||
|
||
type Flags struct {
|
||
Contents string `xml:",innerxml"`
|
||
}
|
||
|
||
type ProjectMeta struct {
|
||
XMLName xml.Name `xml:"project"`
|
||
Name string `xml:"name,attr"`
|
||
Title string `xml:"title"`
|
||
Description string `xml:"description"`
|
||
Url string `xml:"url"`
|
||
ScmSync string `xml:"scmsync"`
|
||
Repositories []RepositoryMeta `xml:"repository"`
|
||
|
||
BuildFlags Flags `xml:"build"`
|
||
PublicFlags Flags `xml:"publish"`
|
||
DebugFlags Flags `xml:"debuginfo"`
|
||
UseForBuild Flags `xml:"useforbuild"`
|
||
}
|
||
|
||
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)
|
||
log.Printf("request: %#v", *req.URL)
|
||
log.Printf("headers: %#v", req.Header)
|
||
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)
|
||
}
|
||
|
||
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:]
|
||
// no UTF-8 in OBS :(
|
||
// prjname = "_" + prjname[1:]
|
||
// case ':':
|
||
// prjname = ":" + prjname[1:]
|
||
// case '.':
|
||
// prjname = "․" + prjname[1:]
|
||
}
|
||
|
||
return prjname
|
||
}
|
||
|
||
var ValidBlockModes []string
|
||
var ValidPrjLinkModes []string
|
||
var ValidTriggerModes []string
|
||
|
||
func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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
|
||
|
||
}
|
||
|
||
type PackageBuildStatus struct {
|
||
Package string `xml:"package,attr"`
|
||
Code string `xml:"code,attr"`
|
||
Details string `xml:"details"`
|
||
}
|
||
|
||
type BuildResult struct {
|
||
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"`
|
||
}
|
||
|
||
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"`
|
||
}
|
||
|
||
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) {
|
||
if r == nil {
|
||
return true, true
|
||
}
|
||
|
||
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() {
|
||
ValidTriggerModes = []string{"transitive", "direct", "local"}
|
||
ValidBlockModes = []string{"all", "local", "never"}
|
||
ValidPrjLinkModes = []string{"off", "localdep", "alldirect", "all"}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
func parseBuildResults(data []byte) (*BuildResultList, error) {
|
||
result := BuildResultList{}
|
||
err := xml.Unmarshal(data, &result)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
type ObsProjectNotFound struct {
|
||
Project string
|
||
}
|
||
|
||
func (obs ObsProjectNotFound) Error() string {
|
||
return fmt.Sprintf("OBS project is not found: %s", obs.Project)
|
||
}
|
||
|
||
func (c *ObsClient) BuildStatus(project string, packages ...string) (*BuildResultList, error) {
|
||
u := c.baseUrl.JoinPath("build", project, "_result")
|
||
query := u.Query()
|
||
query.Add("view", "status")
|
||
query.Add("view", "binarylist")
|
||
query.Add("multibuild", "1")
|
||
if len(packages) > 0 {
|
||
query.Add("lastbuild", "1")
|
||
for _, pkg := range packages {
|
||
query.Add("package", pkg)
|
||
}
|
||
}
|
||
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
|
||
}
|
||
|
||
switch res.StatusCode {
|
||
case 200:
|
||
break
|
||
case 404:
|
||
return nil, ObsProjectNotFound{project}
|
||
default:
|
||
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)
|
||
}
|