package distro import ( "bufio" "encoding/json" "errors" "fmt" "io" "io/ioutil" "os" "sort" "strings" "github.com/osbuild/osbuild-composer/internal/blueprint" "github.com/osbuild/osbuild-composer/internal/rpmmd" ) // A Distro represents composer's notion of what a given distribution is. type Distro interface { // Returns the name of the distro. Name() string // Returns the module platform id of the distro. This is used by DNF // for modularity support. ModulePlatformID() string // Returns a sorted list of the names of the architectures this distro // supports. ListArches() []string // Returns an object representing the given architecture as support // by this distro. GetArch(arch string) (Arch, error) } // An Arch represents a given distribution's support for a given architecture. type Arch interface { // Returns the name of the architecture. Name() string // Returns a sorted list of the names of the image types this architecture // supports. ListImageTypes() []string // Returns an object representing a given image format for this architecture, // on this distro. GetImageType(imageType string) (ImageType, error) // Returns the parent distro Distro() Distro } // An ImageType represents a given distribution's support for a given Image Type // for a given architecture. type ImageType interface { // Returns the name of the image type. Name() string // Returns the parent architecture Arch() Arch // Returns the canonical filename for the image type. Filename() string // Retrns the MIME-type for the image type. MIMEType() string // Returns the default OSTree ref for the image type. OSTreeRef() string // Returns the proper image size for a given output format. If the input size // is 0 the default value for the format will be returned. Size(size uint64) uint64 // Returns the sets of packages to include and exclude when building the image. // Indexed by a string label. How each set is labeled and used depends on the // image type. PackageSets(bp blueprint.Blueprint) map[string]rpmmd.PackageSet // Returns the names of the stages that will produce the build output. Exports() []string // Returns an osbuild manifest, containing the sources and pipeline necessary // to build an image, given output format with all packages and customizations // specified in the given blueprint. The packageSpecSets must be labelled in // the same way as the originating PackageSets. Manifest(b *blueprint.Customizations, options ImageOptions, repos []rpmmd.RepoConfig, packageSpecSets map[string][]rpmmd.PackageSpec, seed int64) (Manifest, error) } // The ImageOptions specify options for a specific image build type ImageOptions struct { OSTree OSTreeImageOptions Size uint64 Subscription *SubscriptionImageOptions } // The OSTreeImageOptions specify ostree-specific image options type OSTreeImageOptions struct { Ref string Parent string URL string } // The SubscriptionImageOptions specify subscription-specific image options // ServerUrl denotes the host to register the system with // BaseUrl specifies the repository URL for DNF type SubscriptionImageOptions struct { Organization int ActivationKey string ServerUrl string BaseUrl string Insights bool } // A Manifest is an opaque JSON object, which is a valid input to osbuild type Manifest []byte func (m Manifest) MarshalJSON() ([]byte, error) { return json.RawMessage(m).MarshalJSON() } func (m *Manifest) UnmarshalJSON(payload []byte) error { var raw json.RawMessage err := (&raw).UnmarshalJSON(payload) if err != nil { return err } *m = Manifest(raw) return nil } type Registry struct { distros map[string]Distro } func NewRegistry(distros ...Distro) (*Registry, error) { reg := &Registry{ distros: make(map[string]Distro), } for _, distro := range distros { name := distro.Name() if _, exists := reg.distros[name]; exists { return nil, fmt.Errorf("NewRegistry: passed two distros with the same name: %s", distro.Name()) } reg.distros[name] = distro } return reg, nil } func (r *Registry) GetDistro(name string) Distro { distro, ok := r.distros[name] if !ok { return nil } return distro } // List returns the names of all distros in a Registry, sorted alphabetically. func (r *Registry) List() []string { list := []string{} for _, distro := range r.distros { list = append(list, distro.Name()) } sort.Strings(list) return list } func (r *Registry) FromHost() (Distro, bool, bool, error) { name, beta, isStream, err := GetHostDistroName() if err != nil { return nil, false, false, err } d := r.GetDistro(name) if d == nil { return nil, false, false, errors.New("unknown distro: " + name) } return d, beta, isStream, nil } func GetHostDistroName() (string, bool, bool, error) { f, err := os.Open("/etc/os-release") if err != nil { return "", false, false, err } defer f.Close() osrelease, err := readOSRelease(f) if err != nil { return "", false, false, err } isStream := osrelease["NAME"] == "CentOS Stream" // NOTE: We only consider major releases up until rhel 8.4 version := strings.Split(osrelease["VERSION_ID"], ".") name := osrelease["ID"] + "-" + version[0] if osrelease["ID"] == "rhel" && version[0] == "8" && version[1] >= "4" { name = name + version[1] } // TODO: We should probably index these things by the full CPE beta := strings.Contains(osrelease["CPE_NAME"], "beta") return name, beta, isStream, nil } // GetRedHatRelease returns the content of /etc/redhat-release // without the trailing new-line. func GetRedHatRelease() (string, error) { raw, err := ioutil.ReadFile("/etc/redhat-release") if err != nil { return "", fmt.Errorf("cannot read /etc/redhat-release: %v", err) } //Remove the trailing new-line. redHatRelease := strings.TrimSpace(string(raw)) return redHatRelease, nil } func readOSRelease(r io.Reader) (map[string]string, error) { osrelease := make(map[string]string) scanner := bufio.NewScanner(r) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if len(line) == 0 { continue } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { return nil, errors.New("readOSRelease: invalid input") } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) if value[0] == '"' { if len(value) < 2 || value[len(value)-1] != '"' { return nil, errors.New("readOSRelease: invalid input") } value = value[1 : len(value)-1] } osrelease[key] = value } return osrelease, nil }