debian-forge-composer/vendor/github.com/osbuild/images/pkg/ostree/ostree.go
Tomáš Hozza 7bdd036395 Update osbuild/images to v0.88.0
Adjust all paces that call `Solver.Depsolve()`, to cope with the changes
that enabled SBOM support.

Fix loading of testing repositories in the CloudAPI unit tests.

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
2024-09-20 17:02:09 +02:00

262 lines
7.7 KiB
Go

package ostree
import (
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"regexp"
"strings"
"time"
"github.com/osbuild/images/pkg/rhsm"
)
var (
ostreeRefRE = regexp.MustCompile(`^(?:[\w\d][-._\w\d]*\/)*[\w\d][-._\w\d]*$`)
ostreeCommitRE = regexp.MustCompile("^[0-9a-f]{64}$")
)
// SourceSpec serves as input for ResolveParams, and contains all necessary
// variables to resolve a ref, which can then be turned into a CommitSpec.
type SourceSpec struct {
URL string
Ref string
RHSM bool
}
// CommitSpec specifies an ostree commit using any combination of Ref (branch), URL (source), and Checksum (commit ID).
type CommitSpec struct {
// Ref for the commit. Can be empty.
Ref string
// URL of the repo where the commit can be fetched, if available.
URL string
ContentURL string
Secrets string
// Checksum of the commit.
Checksum string
}
// ImageOptions specify an ostree ref, checksum, URL, ContentURL, and RHSM. The
// meaning of each parameter depends on the image type being built. The type
// is used to specify ostree-related image options when initializing a
// Manifest.
type ImageOptions struct {
// For ostree commit and container types: The ref of the new commit to be
// built.
// For ostree installers and raw images: The ref of the commit being
// embedded in the installer or deployed in the image.
ImageRef string `json:"ref"`
// For ostree commit and container types: The ParentRef specifies the parent
// ostree commit that the new commit will be based on.
// For ostree installers and raw images: The ParentRef does not apply.
ParentRef string `json:"parent"`
// The URL from which to fetch the commit specified by the checksum.
URL string `json:"url"`
// If specified, the URL will be used only for metadata.
ContentURL string `json:"contenturl"`
// Indicate if the 'org.osbuild.rhsm.consumer' secret should be added when pulling from the
// remote.
RHSM bool `json:"rhsm"`
}
// Validate the image options. This doesn't verify the existence of any remote
// objects and does not guarantee that refs will be successfully resolved. It
// only checks that the values and value combinations are valid.
//
// The function checks the following:
// - The ImageRef, if specified, is a valid ref and does not look like a
// checksum.
// - The ParentRef, if specified, must be a valid ref or a checksum.
// - If the ParentRef is specified, the URL must also be specified.
// - URLs must be valid.
func (options ImageOptions) Validate() error {
if ref := options.ImageRef; ref != "" {
// image ref must not look like a checksum
if verifyChecksum(ref) {
return NewRefError("ostree image ref looks like a checksum %q", ref)
}
if !verifyRef(ref) {
return NewRefError("invalid ostree image ref %q", ref)
}
}
if parent := options.ParentRef; parent != "" {
if !verifyChecksum(parent) && !verifyRef(parent) {
return NewRefError("invalid ostree parent ref or commit %q", parent)
}
// valid URL required
if purl := options.URL; purl == "" {
return NewParameterComboError("ostree parent ref specified, but no URL to retrieve it")
}
}
// whether required or not, any URL specified must be valid
if purl := options.URL; purl != "" {
if _, err := url.ParseRequestURI(purl); err != nil {
return fmt.Errorf("ostree URL %q is invalid", purl)
}
}
if curl := options.ContentURL; curl != "" {
if _, err := url.ParseRequestURI(curl); err != nil {
return fmt.Errorf("ostree content URL %q is invalid", curl)
}
}
return nil
}
// Remote defines the options that can be set for an OSTree Remote configuration.
type Remote struct {
Name string
URL string
ContentURL string
GPGKeyPaths []string
}
func verifyRef(ref string) bool {
return len(ref) > 0 && ostreeRefRE.MatchString(ref)
}
func verifyChecksum(commit string) bool {
return len(commit) > 0 && ostreeCommitRE.MatchString(commit)
}
// ResolveRef resolves the URL path specified by the location and ref
// (location+"refs/heads/"+ref) and returns the commit ID for the named ref. If
// there is an error, it will be of type ResolveRefError.
func ResolveRef(location, ref string, consumerCerts bool, subs *rhsm.Subscriptions, ca *string) (string, error) {
u, err := url.Parse(location)
if err != nil {
return "", NewResolveRefError(fmt.Sprintf("error parsing ostree repository location: %v", err))
}
u.Path = path.Join(u.Path, "refs/heads/", ref)
var client *http.Client
if consumerCerts {
if subs == nil {
subs, err = rhsm.LoadSystemSubscriptions()
if err != nil {
return "", NewResolveRefError("error adding rhsm certificates when resolving ref: %s", err)
}
if subs.Consumer == nil {
return "", NewResolveRefError("error adding rhsm certificates when resolving ref")
}
}
tlsConf := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if ca != nil {
caCertPEM, err := os.ReadFile(*ca)
if err != nil {
return "", NewResolveRefError("error adding rhsm certificates when resolving ref: %s", err)
}
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM(caCertPEM)
if !ok {
return "", NewResolveRefError("error adding rhsm certificates when resolving ref")
}
tlsConf.RootCAs = roots
}
cert, err := tls.LoadX509KeyPair(subs.Consumer.ConsumerCert, subs.Consumer.ConsumerKey)
if err != nil {
return "", NewResolveRefError("error adding rhsm certificates when resolving ref: %s", err)
}
tlsConf.Certificates = []tls.Certificate{cert}
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConf,
},
Timeout: 300 * time.Second,
}
} else {
client = &http.Client{}
}
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return "", NewResolveRefError("error preparing ostree resolve request: %s", err)
}
resp, err := client.Do(req)
if err != nil {
return "", NewResolveRefError(fmt.Sprintf("error sending request to ostree repository %q: %v", u.String(), err))
}
if resp.StatusCode != http.StatusOK {
return "", NewResolveRefError("ostree repository %q returned status: %s", u.String(), resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", NewResolveRefError(fmt.Sprintf("error reading response from ostree repository %q: %v", u.String(), err))
}
checksum := strings.TrimSpace(string(body))
// Check that this is at least a hex string.
_, err = hex.DecodeString(checksum)
if err != nil {
return "", NewResolveRefError("ostree repository %q returned invalid reference", u.String())
}
return checksum, nil
}
// Resolve the ostree source specification to a commit specification.
//
// If a URL is defined in the source specification, the checksum of the ref is
// resolved, otherwise the checksum is an empty string. Failure to resolve the
// checksum results in a ResolveRefError.
//
// If the ref is already a checksum (64 alphanumeric characters), it is not
// resolved or checked against the repository.
//
// If the ref is malformed, the function returns with a RefError.
func Resolve(source SourceSpec) (CommitSpec, error) {
commit := CommitSpec{
Ref: source.Ref,
URL: source.URL,
}
if source.RHSM {
commit.Secrets = "org.osbuild.rhsm.consumer"
}
if verifyChecksum(source.Ref) {
// the ref is a commit: return as is
commit.Checksum = source.Ref
return commit, nil
}
if !verifyRef(source.Ref) {
// the ref is not a commit and it's also an invalid ref
return CommitSpec{}, NewRefError("Invalid ostree ref or commit %q", source.Ref)
}
// URL set: Resolve checksum
if source.URL != "" {
// If a URL is specified, we need to fetch the commit at the URL.
checksum, err := ResolveRef(source.URL, source.Ref, source.RHSM, nil, nil)
if err != nil {
return CommitSpec{}, err // ResolveRefError
}
commit.Checksum = checksum
}
return commit, nil
}