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 }