github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/imagedefinition/image_definition.go (about) 1 /* 2 Package imagedefinition provides the structure for the 3 image definition that will be parsed from a YAML file. 4 */ 5 package imagedefinition 6 7 import ( 8 "fmt" 9 "strings" 10 11 "github.com/xeipuuv/gojsonschema" 12 ) 13 14 // ImageDefinition is the parent struct for the data 15 // contained within a classic image definition file 16 type ImageDefinition struct { 17 ImageName string `yaml:"name" json:"ImageName"` 18 DisplayName string `yaml:"display-name" json:"DisplayName"` 19 Revision int `yaml:"revision" json:"Revision,omitempty"` 20 Architecture string `yaml:"architecture" json:"Architecture"` 21 Series string `yaml:"series" json:"Series"` 22 Kernel string `yaml:"kernel" json:"Kernel,omitempty"` 23 Gadget *Gadget `yaml:"gadget" json:"Gadget,omitempty"` 24 ModelAssertion string `yaml:"model-assertion" json:"ModelAssertion,omitempty" jsonschema:"type=string,format=uri"` 25 Rootfs *Rootfs `yaml:"rootfs" json:"Rootfs"` 26 Customization *Customization `yaml:"customization" json:"Customization,omitempty"` 27 Artifacts *Artifact `yaml:"artifacts" json:"Artifacts,omitempty"` 28 Class string `yaml:"class" json:"Class" jsonschema:"enum=preinstalled,enum=cloud,enum=installer"` 29 } 30 31 // Gadget defines the gadget section of the image definition file 32 type Gadget struct { 33 Ref string `yaml:"ref" json:"Ref,omitempty"` 34 GadgetTarget string `yaml:"target" json:"GadgetTarget,omitempty"` 35 GadgetBranch string `yaml:"branch" json:"GadgetBranch,omitempty"` 36 GadgetType string `yaml:"type" json:"GadgetType" jsonschema:"enum=git,enum=directory,enum=prebuilt"` 37 GadgetURL string `yaml:"url" json:"GadgetURL,omitempty" jsonschema:"type=string,format=uri"` 38 } 39 40 // Rootfs defines the rootfs section of the image definition file 41 type Rootfs struct { 42 Components []string `yaml:"components" json:"Components,omitempty" default:"main,restricted"` 43 Archive string `yaml:"archive" json:"Archive" default:"ubuntu"` 44 Flavor string `yaml:"flavor" json:"Flavor" default:"ubuntu"` 45 Mirror string `yaml:"mirror" json:"Mirror" default:"http://archive.ubuntu.com/ubuntu/"` 46 Pocket string `yaml:"pocket" json:"Pocket" jsonschema:"enum=release,enum=Release,enum=updates,enum=Updates,enum=security,enum=Security,enum=proposed,enum=Proposed" default:"release"` 47 Seed *Seed `yaml:"seed" json:"Seed,omitempty" jsonschema:"oneof_required=Seed"` 48 Tarball *Tarball `yaml:"tarball" json:"Tarball,omitempty" jsonschema:"oneof_required=Tarball"` 49 ArchiveTasks []string `yaml:"archive-tasks" json:"ArchiveTasks,omitempty" jsonschema:"oneof_required=ArchiveTasks"` 50 SourcesListDeb822 *bool `yaml:"sources-list-deb822" json:"SourcesListDeb822" default:"false"` 51 } 52 53 // Seed defines the seed section of rootfs, which is used to 54 // build a rootfs via seed germination 55 type Seed struct { 56 SeedBranch string `yaml:"branch" json:"SeedBranch,omitempty"` 57 SeedURLs []string `yaml:"urls" json:"SeedURLs" jsonschema:"type=array,format=uri"` 58 Names []string `yaml:"names" json:"Names"` 59 Vcs *bool `yaml:"vcs" json:"Vcs" default:"true"` 60 } 61 62 // Tarball defines the tarball section of rootfs, which is used 63 // to create images from a pre-built rootfs 64 type Tarball struct { 65 TarballURL string `yaml:"url" json:"TarballURL" jsonschema:"type=string,format=uri"` 66 GPG string `yaml:"gpg" json:"GPG,omitempty" jsonschema:"type=string,format=uri"` 67 SHA256sum string `yaml:"sha256sum" json:"SHA256sum,omitempty" jsonschema:"minLength=64,maxLength=64"` 68 } 69 70 // Customization defines the customization section of the image definition file. 71 type Customization struct { 72 Components []string `yaml:"components" json:"Components,omitempty" default:"main,restricted,universe"` 73 Pocket string `yaml:"pocket" json:"Pocket" jsonschema:"enum=release,enum=Release,enum=updates,enum=Updates,enum=security,enum=Security,enum=proposed,enum=Proposed" default:"release"` 74 Installer *Installer `yaml:"installer" json:"Installer,omitempty"` 75 CloudInit *CloudInit `yaml:"cloud-init" json:"CloudInit,omitempty"` 76 ExtraPPAs []*PPA `yaml:"extra-ppas" json:"ExtraPPAs,omitempty"` 77 ExtraPackages []*Package `yaml:"extra-packages" json:"ExtraPackages,omitempty"` 78 ExtraSnaps []*Snap `yaml:"extra-snaps" json:"ExtraSnaps,omitempty"` 79 Fstab []*Fstab `yaml:"fstab" json:"Fstab,omitempty"` 80 Manual *Manual `yaml:"manual" json:"Manual,omitempty"` 81 } 82 83 // Installer provides customization options specific to installer images 84 type Installer struct { 85 Preseeds []string `yaml:"preseeds" json:"Preseeds,omitempty"` 86 Layers []string `yaml:"layers" json:"Layers,omitempty"` 87 } 88 89 // CloudInit provides customizations for running cloud-init 90 type CloudInit struct { 91 MetaData string `yaml:"meta-data" json:"MetaData,omitempty"` 92 UserData string `yaml:"user-data" json:"UserData,omitempty"` 93 NetworkConfig string `yaml:"network-config" json:"NetworkConfig,omitempty"` 94 } 95 96 // PPA contains information about a public or private PPA 97 type PPA struct { 98 Name string `yaml:"name" json:"PPAName" jsonschema:"pattern=^[a-zA-Z0-9_.+-]+/[a-zA-Z0-9_.+-]+$"` 99 Auth string `yaml:"auth" json:"Auth,omitempty" jsonschema:"pattern=^[a-zA-Z0-9_.+-]+:[a-zA-Z0-9]+$"` 100 Fingerprint string `yaml:"fingerprint" json:"Fingerprint,omitempty"` 101 KeepEnabled *bool `yaml:"keep-enabled" json:"KeepEnabled" default:"true"` 102 } 103 104 // Package contains information about packages 105 type Package struct { 106 PackageName string `yaml:"name" json:"PackageName"` 107 } 108 109 // Snap contains information about snaps 110 type Snap struct { 111 SnapName string `yaml:"name" json:"SnapName"` 112 SnapRevision int `yaml:"revision" json:"SnapRevision,omitempty" jsonschema:"type=integer"` 113 Store string `yaml:"store" json:"Store" default:"canonical"` 114 Channel string `yaml:"channel" json:"Channel" default:"stable"` 115 } 116 117 // Manual provides manual customization options 118 type Manual struct { 119 MakeDirs []*MakeDirs `yaml:"make-dirs" json:"MakeDirs,omitempty"` 120 CopyFile []*CopyFile `yaml:"copy-file" json:"CopyFile,omitempty"` 121 Execute []*Execute `yaml:"execute" json:"Execute,omitempty"` 122 TouchFile []*TouchFile `yaml:"touch-file" json:"TouchFile,omitempty"` 123 AddGroup []*AddGroup `yaml:"add-group" json:"AddGroup,omitempty"` 124 AddUser []*AddUser `yaml:"add-user" json:"AddUser,omitempty"` 125 } 126 127 // Fstab defines the information that gets rendered into an fstab 128 type Fstab struct { 129 Label string `yaml:"label" json:"Label"` 130 Mountpoint string `yaml:"mountpoint" json:"Mountpoint"` 131 FSType string `yaml:"filesystem-type" json:"FSType"` 132 MountOptions string `yaml:"mount-options" json:"MountOptions" default:"defaults"` 133 Dump bool `yaml:"dump" json:"Dump,omitempty"` 134 FsckOrder int `yaml:"fsck-order" json:"FsckOrder"` 135 } 136 137 // MakeDirs allows users to copy files into the rootfs of an image 138 type MakeDirs struct { 139 Path string `yaml:"path" json:"Path"` 140 Permissions uint32 `yaml:"permissions" json:"Permissions" default:"0755"` 141 } 142 143 // CopyFile allows users to copy files into the rootfs of an image 144 type CopyFile struct { 145 Dest string `yaml:"destination" json:"Dest"` 146 Source string `yaml:"source" json:"Source"` 147 } 148 149 // Execute allows users to execute a script in the rootfs of an image 150 type Execute struct { 151 ExecutePath string `yaml:"path" json:"ExecutePath"` 152 } 153 154 // TouchFile allows users to touch a file in the rootfs of an image 155 type TouchFile struct { 156 TouchPath string `yaml:"path" json:"TouchPath"` 157 } 158 159 // AddGroup allows users to add a group in the image that is being built 160 type AddGroup struct { 161 GroupName string `yaml:"name" json:"GroupName"` 162 GroupID string `yaml:"id" json:"GroupID,omitempty"` 163 } 164 165 // AddUser allows users to add a user in the image that is being built 166 type AddUser struct { 167 UserName string `yaml:"name" json:"UserName"` 168 UserID string `yaml:"id" json:"UserID,omitempty"` 169 Password string `yaml:"password" json:"Password,omitempty"` 170 PasswordType string `yaml:"password-type" json:"PasswordType" default:"hash" jsonschema:"enum=text,enum=hash"` 171 } 172 173 // Artifact contains information about the files that are created 174 // during and as a result of the image build process 175 type Artifact struct { 176 Img *[]Img `yaml:"img" json:"Img,omitempty" is_disk:"true"` 177 Iso *[]Iso `yaml:"iso" json:"Iso,omitempty" is_disk:"true"` 178 Qcow2 *[]Qcow2 `yaml:"qcow2" json:"Qcow2,omitempty" is_disk:"true"` 179 Manifest *Manifest `yaml:"manifest" json:"Manifest,omitempty" is_disk:"false"` 180 Filelist *Filelist `yaml:"filelist" json:"Filelist,omitempty" is_disk:"false"` 181 Changelog *Changelog `yaml:"changelog" json:"Changelog,omitempty" is_disk:"false"` 182 RootfsTar *RootfsTar `yaml:"rootfs-tarball" json:"RootfsTar,omitempty" is_disk:"false"` 183 } 184 185 // Img specifies the name of the resulting .img file. 186 // If left emtpy no .img file will be created 187 type Img struct { 188 ImgName string `yaml:"name" json:"ImgName"` 189 ImgVolume string `yaml:"volume" json:"ImgVolume"` 190 } 191 192 // Iso specifies the name of the resulting .iso file 193 // and optionally the xorrisofs command used to create it. 194 // If left emtpy no .iso file will be created 195 type Iso struct { 196 IsoName string `yaml:"name" json:"IsoName"` 197 IsoVolume string `yaml:"volume" json:"IsoVolume"` 198 Command string `yaml:"xorriso-command" json:"Command,omitempty"` 199 } 200 201 // Qcow2 specifies the name of the resulting .qcow2 file 202 // If left emtpy no .qcow2 file will be created 203 type Qcow2 struct { 204 Qcow2Name string `yaml:"name" json:"Qcow2Name"` 205 Qcow2Volume string `yaml:"volume" json:"Qcow2Volume"` 206 } 207 208 // Manifest specifies the name of the manifest file. 209 // If left emtpy no manifest file will be created 210 type Manifest struct { 211 ManifestName string `yaml:"name" json:"ManifestName"` 212 } 213 214 // Filelist specifies the name of the filelist file. 215 // If left emtpy no filelist file will be created 216 type Filelist struct { 217 FilelistName string `yaml:"name" json:"FilelistName"` 218 } 219 220 // Changelog specifies the name of the changelog file. 221 // If left emtpy no changelog file will be created 222 type Changelog struct { 223 ChangelogName string `yaml:"name" json:"ChangelogName"` 224 } 225 226 // RootfsTar specifies the name of a tarball to create from the 227 // rootfs build steps and the compression to use on it 228 type RootfsTar struct { 229 RootfsTarName string `yaml:"name" json:"RootfsTarName"` 230 Compression string `yaml:"compression" json:"Compression" jsonschema:"enum=uncompressed,enum=bzip2,enum=gzip,enum=xz,enum=zstd" default:"uncompressed"` 231 } 232 233 // NewMissingURLError fails the image definition parsing when a dict 234 // requires a URL conditionally based on the value of other keys 235 // in the dict but does not have one included 236 func NewMissingURLError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *MissingURLError { 237 err := MissingURLError{} 238 err.SetContext(context) 239 err.SetType("missing_url_error") 240 err.SetDescriptionFormat("When key {{.key}} is specified as {{.value}}, a URL must be provided") 241 err.SetValue(value) 242 err.SetDetails(details) 243 244 return &err 245 } 246 247 // MissingURLError implements gojsonschema.ErrorType. It is used for custom errors for 248 // fields that require a url based on the value of other fields 249 // based on the values in other fields 250 type MissingURLError struct { 251 gojsonschema.ResultErrorFields 252 } 253 254 // NewInvalidPPAError fails the image definition parsing when a private PPA 255 // is configured with no fingerprint 256 func NewInvalidPPAError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *InvalidPPAError { 257 err := InvalidPPAError{} 258 err.SetContext(context) 259 err.SetType("private_ppa_without_fingerprint") 260 err.SetDescriptionFormat("Fingerprint is required for private PPAs") 261 err.SetValue(value) 262 err.SetDetails(details) 263 264 return &err 265 } 266 267 // InvalidPPAError implements gojsonschema.ErrorType. It is used for custom errors 268 // when a private PPA does not have a fingerprint specified 269 type InvalidPPAError struct { 270 gojsonschema.ResultErrorFields 271 } 272 273 // NewPathNotAbsoluteError fails the image definition parsing when a relative path is given 274 func NewPathNotAbsoluteError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *PathNotAbsoluteError { 275 err := PathNotAbsoluteError{} 276 err.SetContext(context) 277 err.SetType("path_not_absolute_error") 278 err.SetDescriptionFormat("Key {{.key}} needs to be an absolute path ({{.value}})") 279 err.SetValue(value) 280 err.SetDetails(details) 281 282 return &err 283 } 284 285 // PathNotAbsoluteError implements gojsonschema.ErrorType. It is used for custom errors for 286 // fields that should be absolute but are not 287 type PathNotAbsoluteError struct { 288 gojsonschema.ResultErrorFields 289 } 290 291 // NewDependentKeyError fails the image definition parsing when one 292 // field depends on another being specified 293 func NewDependentKeyError(context *gojsonschema.JsonContext, value interface{}, details gojsonschema.ErrorDetails) *DependentKeyError { 294 err := DependentKeyError{} 295 err.SetContext(context) 296 err.SetType("dependent_key_error") 297 err.SetDescriptionFormat("Key {{.key1}} cannot be used without key {{.key2}}") 298 err.SetValue(value) 299 err.SetDetails(details) 300 301 return &err 302 } 303 304 // DependentKeyError implements gojsonschema.ErrorType. 305 // It is used for custom errors for keys that depend on 306 // other keys being specified 307 type DependentKeyError struct { 308 gojsonschema.ResultErrorFields 309 } 310 311 func (i ImageDefinition) securityMirror() string { 312 if i.Architecture == "amd64" || i.Architecture == "i386" { 313 return "http://security.ubuntu.com/ubuntu/" 314 } 315 return i.Rootfs.Mirror 316 } 317 318 // generateLegacySourcesList returns the content to write to the sources.list file 319 // under the legacy format. 320 func generateLegacySourcesList(series string, components []string, mirror string, securityMirror string, pocket string) string { 321 baseList := fmt.Sprintf("deb %%s %s%%s %s", series, strings.Join(components, " ")) 322 323 releaseSourceComment := `# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to 324 # newer versions of the distribution. 325 ` 326 updatesSourceComment := `## Major bug fix updates produced after the final release of the 327 ## distribution. 328 ` 329 330 releaseSource := releaseSourceComment + fmt.Sprintf(baseList, mirror, "") 331 securitySource := fmt.Sprintf(baseList, securityMirror, "-security") 332 updatesSource := updatesSourceComment + fmt.Sprintf(baseList, mirror, "-updates") 333 proposedSource := fmt.Sprintf(baseList, mirror, "-proposed") 334 335 sourcesList := make([]string, 0) 336 337 switch pocket { 338 case "release": 339 sourcesList = append(sourcesList, releaseSource) 340 case "security": 341 sourcesList = append(sourcesList, releaseSource, securitySource) 342 case "updates": 343 sourcesList = append(sourcesList, releaseSource, securitySource, updatesSource) 344 case "proposed": 345 sourcesList = append(sourcesList, releaseSource, securitySource, updatesSource, proposedSource) 346 } 347 348 return strings.Join(sourcesList, "\n") + "\n" 349 } 350 351 // LegacyBuildSourcesList returns the content of the /etc/apt/sources.list to be used 352 // during the build process 353 func (i *ImageDefinition) LegacyBuildSourcesList() string { 354 return i.legacySourcesList(false) 355 } 356 357 // LegacyTargetSourcesList returns the content of the /etc/apt/sources.list for the target 358 // image 359 func (i *ImageDefinition) LegacyTargetSourcesList() string { 360 return i.legacySourcesList(true) 361 } 362 363 // legacySourcesList returns the content of the /etc/apt/sources.list file in the 364 // legacy format (not deb822). 365 func (i *ImageDefinition) legacySourcesList(target bool) string { 366 pocket := i.Rootfs.Pocket 367 if target { 368 pocket = i.Customization.Pocket 369 } 370 371 return generateLegacySourcesList( 372 i.Series, 373 i.Customization.Components, 374 i.Rootfs.Mirror, 375 i.securityMirror(), 376 strings.ToLower(pocket)) 377 } 378 379 // generateDeb822Section returns a deb822 section/paragraph to be used in a sources list file 380 // This function is tailored to what is expected in an official ubuntu image and should not be 381 // used as is to generate arbitrary deb822 sections. 382 func generateDeb822Section(mirror string, series string, components []string, pocket string) string { 383 sectionTmpl := `Types: deb 384 URIs: %s 385 Suites: %s 386 Components: %s 387 Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg 388 389 ` 390 391 suites := make([]string, 0) 392 393 switch pocket { 394 case "security": 395 suites = []string{series + "-security"} 396 case "proposed": 397 suites = append([]string{series + "-proposed"}, suites...) 398 fallthrough 399 case "updates": 400 suites = append([]string{series + "-updates"}, suites...) 401 fallthrough 402 case "release": 403 suites = append([]string{series}, suites...) 404 } 405 406 return fmt.Sprintf(sectionTmpl, 407 mirror, 408 strings.Join(suites, " "), 409 strings.Join(components, " "), 410 ) 411 } 412 413 var LegacySourcesListComment = `# Ubuntu sources have moved to the /etc/apt/sources.list.d/ubuntu.sources 414 # file, which uses the deb822 format. Use deb822-formatted .sources files 415 # to manage package sources in the /etc/apt/sources.list.d/ directory. 416 # See the sources.list(5) manual page for details. 417 ` 418 419 var ubuntuSourceHeader = `## Ubuntu distribution repository 420 ## 421 ## The following settings can be adjusted to configure which packages to use from Ubuntu. 422 ## Mirror your choices (except for URIs and Suites) in the security section below to 423 ## ensure timely security updates. 424 ## 425 ## Types: Append deb-src to enable the fetching of source package. 426 ## URIs: A URL to the repository (you may add multiple URLs) 427 ## Suites: The following additional suites can be configured 428 ## <name>-updates - Major bug fix updates produced after the final release of the 429 ## distribution. 430 ## <name>-backports - software from this repository may not have been tested as 431 ## extensively as that contained in the main release, although it includes 432 ## newer versions of some applications which may provide useful features. 433 ## Also, please note that software in backports WILL NOT receive any review 434 ## or updates from the Ubuntu security team. 435 ## Components: Aside from main, the following components can be added to the list 436 ## restricted - Software that may not be under a free license, or protected by patents. 437 ## universe - Community maintained packages. Software in this repository receives maintenance 438 ## from volunteers in the Ubuntu community, or a 10 year security maintenance 439 ## commitment from Canonical when an Ubuntu Pro subscription is attached. 440 ## multiverse - Community maintained of restricted. Software from this repository is 441 ## ENTIRELY UNSUPPORTED by the Ubuntu team, and may not be under a free 442 ## licence. Please satisfy yourself as to your rights to use the software. 443 ## Also, please note that software in multiverse WILL NOT receive any 444 ## review or updates from the Ubuntu security team. 445 ## 446 ## See the sources.list(5) manual page for further settings. 447 ` 448 449 var ubuntuSourceSecurityHeader = `## Ubuntu security updates. Aside from URIs and Suites, 450 ## this should mirror your choices in the previous section. 451 ` 452 453 // deb822SourcesList returns the content of /etc/apt/sources.list.d/ubuntu.sources 454 // to be used during the build process 455 func (i *ImageDefinition) Deb822BuildSourcesList() string { 456 return i.deb822SourcesList(false) 457 } 458 459 // deb822SourcesList returns the content of /etc/apt/sources.list.d/ubuntu.sources 460 // for the target image 461 func (i *ImageDefinition) Deb822TargetSourcesList() string { 462 return i.deb822SourcesList(true) 463 } 464 465 // deb822SourcesList returns the content of /etc/apt/sources.list.d/ubuntu.sources 466 // in the deb822 format. 467 // The target param defines if the generated sources list will be used in the target image. 468 func (i *ImageDefinition) deb822SourcesList(target bool) string { 469 pocket := i.Rootfs.Pocket 470 if target { 471 pocket = i.Customization.Pocket 472 } 473 pocket = strings.ToLower(pocket) 474 475 ubuntuSources := ubuntuSourceHeader + generateDeb822Section( 476 i.Rootfs.Mirror, 477 i.Series, 478 i.Rootfs.Components, 479 pocket, 480 ) 481 482 ubuntuSources += ubuntuSourceSecurityHeader + generateDeb822Section( 483 i.securityMirror(), 484 i.Series, 485 i.Rootfs.Components, 486 pocket, 487 ) 488 489 return ubuntuSources 490 }