github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/base/base.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package base
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/charm/v12"
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  )
    14  
    15  // Base represents an OS/Channel.
    16  // Bases can also be converted to and from a series string.
    17  type Base struct {
    18  	OS string
    19  	// Channel is track[/risk/branch].
    20  	// eg "22.04" or "22.04/stable" etc.
    21  	Channel Channel
    22  }
    23  
    24  const (
    25  	// UbuntuOS is the special value to be places in OS field of a base to
    26  	// indicate an operating system is an Ubuntu distro
    27  	UbuntuOS = "ubuntu"
    28  
    29  	// CentosOS is the special value to be places in OS field of a base to
    30  	// indicate an operating system is a CentOS distro
    31  	CentosOS = "centos"
    32  )
    33  
    34  // ParseBase constructs a Base from the os and channel string.
    35  func ParseBase(os string, channel string) (Base, error) {
    36  	if os == "" && channel == "" {
    37  		return Base{}, nil
    38  	}
    39  	if os == "" || channel == "" {
    40  		return Base{}, errors.NotValidf("missing base os or channel")
    41  	}
    42  	ch, err := ParseChannelNormalize(channel)
    43  	if err != nil {
    44  		return Base{}, errors.Annotatef(err, "parsing base %s@%s", os, channel)
    45  	}
    46  	return Base{OS: strings.ToLower(os), Channel: ch}, nil
    47  }
    48  
    49  // ParseBaseFromString takes a string containing os and channel separated
    50  // by @ and returns a base.
    51  func ParseBaseFromString(b string) (Base, error) {
    52  	parts := strings.Split(b, "@")
    53  	if len(parts) != 2 {
    54  		return Base{}, errors.New("expected base string to contain os and channel separated by '@'")
    55  	}
    56  	channel, err := ParseChannelNormalize(parts[1])
    57  	if err != nil {
    58  		return Base{}, errors.Trace(err)
    59  	}
    60  	return Base{OS: parts[0], Channel: channel}, nil
    61  }
    62  
    63  // ParseManifestBases transforms charm.Bases to Bases. This
    64  // format comes out of a charm.Manifest and contains architectures
    65  // which Base does not. Only unique non architecture Bases
    66  // will be returned.
    67  func ParseManifestBases(manifestBases []charm.Base) ([]Base, error) {
    68  	if len(manifestBases) == 0 {
    69  		return nil, errors.BadRequestf("base len zero")
    70  	}
    71  	bases := make([]Base, 0)
    72  	unique := set.NewStrings()
    73  	for _, m := range manifestBases {
    74  		// The data actually comes over the wire as an operating system
    75  		// with a single architecture, not multiple ones.
    76  		// TODO - (hml) 2023-05-18
    77  		// There is no guarantee that every architecture has
    78  		// the same operating systems. This logic should be
    79  		// investigated.
    80  		m.Architectures = []string{}
    81  		if unique.Contains(m.String()) {
    82  			continue
    83  		}
    84  		base, err := ParseBase(m.Name, m.Channel.String())
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  		bases = append(bases, base)
    89  		unique.Add(m.String())
    90  	}
    91  	return bases, nil
    92  }
    93  
    94  // MustParseBaseFromString is like ParseBaseFromString but panics if the string
    95  // is invalid.
    96  func MustParseBaseFromString(b string) Base {
    97  	base, err := ParseBaseFromString(b)
    98  	if err != nil {
    99  		panic(err)
   100  	}
   101  	return base
   102  }
   103  
   104  // MakeDefaultBase creates a base from an os and simple version string, eg "22.04".
   105  func MakeDefaultBase(os string, channel string) Base {
   106  	return Base{OS: os, Channel: MakeDefaultChannel(channel)}
   107  }
   108  
   109  // Empty returns true if the base is empty.
   110  func (b Base) Empty() bool {
   111  	return b.OS == "" && b.Channel.Empty()
   112  }
   113  
   114  func (b Base) String() string {
   115  	if b.OS == "" {
   116  		return ""
   117  	}
   118  	return fmt.Sprintf("%s@%s", b.OS, b.Channel)
   119  }
   120  
   121  // IsCompatible returns true if base other is the same underlying
   122  // OS version, ignoring risk.
   123  func (b Base) IsCompatible(other Base) bool {
   124  	return b.OS == other.OS && b.Channel.Track == other.Channel.Track
   125  }
   126  
   127  // DisplayString returns the base string ignoring risk.
   128  func (b Base) DisplayString() string {
   129  	if b.Channel.Track == "" || b.OS == "" {
   130  		return ""
   131  	}
   132  	if b.OS == Kubernetes.String() {
   133  		return b.OS
   134  	}
   135  	return b.OS + "@" + b.Channel.DisplayString()
   136  }
   137  
   138  // GetBaseFromSeries returns the Base infor for a series.
   139  func GetBaseFromSeries(series string) (Base, error) {
   140  	var result Base
   141  	osName, err := GetOSFromSeries(series)
   142  	if err != nil {
   143  		return result, errors.NotValidf("series %q", series)
   144  	}
   145  	osVersion, err := SeriesVersion(series)
   146  	if err != nil {
   147  		return result, errors.NotValidf("series %q", series)
   148  	}
   149  	result.OS = strings.ToLower(osName.String())
   150  	result.Channel = MakeDefaultChannel(osVersion)
   151  	return result, nil
   152  }
   153  
   154  // GetSeriesFromChannel gets the series from os name and channel.
   155  func GetSeriesFromChannel(name string, channel string) (string, error) {
   156  	base, err := ParseBase(name, channel)
   157  	if err != nil {
   158  		return "", errors.Trace(err)
   159  	}
   160  	return GetSeriesFromBase(base)
   161  }
   162  
   163  // GetSeriesFromBase returns the series name for a
   164  // given Base. This is needed to support legacy series.
   165  func GetSeriesFromBase(v Base) (string, error) {
   166  	var osSeries map[SeriesName]seriesVersion
   167  	switch strings.ToLower(v.OS) {
   168  	case UbuntuOS:
   169  		osSeries = ubuntuSeries
   170  	case CentosOS:
   171  		osSeries = centosSeries
   172  	}
   173  	for s, vers := range osSeries {
   174  		if vers.Version == v.Channel.Track {
   175  			return string(s), nil
   176  		}
   177  	}
   178  	return "", errors.NotFoundf("os %q version %q", v.OS, v.Channel.Track)
   179  }
   180  
   181  // LegacyKubernetesBase is the ubuntu base image for legacy k8s charms.
   182  func LegacyKubernetesBase() Base {
   183  	return MakeDefaultBase(UbuntuOS, "20.04")
   184  }
   185  
   186  // LegacyKubernetesSeries is the ubuntu series for legacy k8s charms.
   187  func LegacyKubernetesSeries() string {
   188  	return "focal"
   189  }