github.com/openshift/installer@v1.4.17/pkg/asset/imagebased/image/imagebased_config.go (about) 1 package image 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/url" 9 "os" 10 "path/filepath" 11 "strings" 12 13 dockerref "github.com/containers/image/v5/docker/reference" 14 "github.com/sirupsen/logrus" 15 "k8s.io/apimachinery/pkg/util/validation/field" 16 "sigs.k8s.io/yaml" 17 18 "github.com/openshift/installer/pkg/asset" 19 "github.com/openshift/installer/pkg/asset/agent/manifests/staticnetworkconfig" 20 "github.com/openshift/installer/pkg/types/imagebased" 21 "github.com/openshift/installer/pkg/validate" 22 ) 23 24 var ( 25 configFilename = "image-based-installation-config.yaml" 26 27 defaultExtraPartitionLabel = "varlibcontainers" 28 defaultExtraPartitionStart = "-40G" 29 defaultExtraPartitionNumber = uint(5) 30 ) 31 32 // ImageBasedInstallationConfig reads the image-based-installation-config.yaml file. 33 type ImageBasedInstallationConfig struct { // nolint:revive // although this name stutters it is useful to convey that it's an image-based installer related struct 34 File *asset.File 35 Config *imagebased.InstallationConfig 36 Template string 37 } 38 39 var _ asset.WritableAsset = (*ImageBasedInstallationConfig)(nil) 40 41 // Name returns a human friendly name for the asset. 42 func (*ImageBasedInstallationConfig) Name() string { 43 return "Image-based Installation ISO Config" 44 } 45 46 // Dependencies returns all of the dependencies directly needed to generate 47 // the asset. 48 func (*ImageBasedInstallationConfig) Dependencies() []asset.Asset { 49 return []asset.Asset{} 50 } 51 52 // Generate generates the Image-based Installation Config YAML manifest. 53 func (i *ImageBasedInstallationConfig) Generate(_ context.Context, dependencies asset.Parents) error { 54 configTemplate := `# 55 # Note: This is a sample ImageBasedInstallationConfig file showing 56 # which fields are available to aid you in creating your 57 # own image-based-installation-config.yaml file. 58 # 59 apiVersion: v1beta1 60 kind: ImageBasedInstallationConfig 61 metadata: 62 name: example-image-based-installation-config 63 # The following fields are required 64 seedImage: quay.io/openshift-kni/seed-image:4.16.0 65 seedVersion: 4.16.0 66 installationDisk: /dev/vda 67 pullSecret: '<your-pull-secret>' 68 # networkConfig is optional and contains the network configuration for the host in NMState format. 69 # See https://nmstate.io/examples.html for examples. 70 # networkConfig: 71 # interfaces: 72 # - name: eth0 73 # type: ethernet 74 # state: up 75 # mac-address: 00:00:00:00:00:00 76 # ipv4: 77 # enabled: true 78 # address: 79 # - ip: 192.168.122.2 80 # prefix-length: 23 81 # dhcp: false 82 ` 83 84 i.Template = configTemplate 85 86 // Set the File field correctly with the generated image-based installation config YAML content. 87 i.File = &asset.File{ 88 Filename: configFilename, 89 Data: []byte(i.Template), 90 } 91 92 return nil 93 } 94 95 // PersistToFile writes the image-based-installation-config.yaml file to the assets folder. 96 func (i *ImageBasedInstallationConfig) PersistToFile(directory string) error { 97 if i.File == nil { 98 return nil 99 } 100 101 configPath := filepath.Join(directory, configFilename) 102 err := os.WriteFile(configPath, i.File.Data, 0o600) 103 if err != nil { 104 return err 105 } 106 107 return nil 108 } 109 110 // Files returns the files generated by the asset. 111 func (i *ImageBasedInstallationConfig) Files() []*asset.File { 112 if i.File != nil { 113 return []*asset.File{i.File} 114 } 115 return []*asset.File{} 116 } 117 118 // Load returns image-based installation ISO config asset from the disk. 119 func (i *ImageBasedInstallationConfig) Load(f asset.FileFetcher) (bool, error) { 120 file, err := f.FetchByName(configFilename) 121 if err != nil { 122 if os.IsNotExist(err) { 123 return false, nil 124 } 125 return false, fmt.Errorf("failed to load %s file: %w", configFilename, err) 126 } 127 config := &imagebased.InstallationConfig{ 128 ExtraPartitionLabel: defaultExtraPartitionLabel, 129 ExtraPartitionStart: defaultExtraPartitionStart, 130 ExtraPartitionNumber: defaultExtraPartitionNumber, 131 } 132 if err := yaml.UnmarshalStrict(file.Data, config); err != nil { 133 return false, fmt.Errorf("failed to unmarshal %s: %w", configFilename, err) 134 } 135 136 i.File, i.Config = file, config 137 if err = i.finish(); err != nil { 138 return false, err 139 } 140 return true, nil 141 } 142 143 func (i *ImageBasedInstallationConfig) finish() error { 144 if err := i.validate().ToAggregate(); err != nil { 145 return fmt.Errorf("invalid Image-based Installation ISO Config: %w", err) 146 } 147 return nil 148 } 149 150 func (i *ImageBasedInstallationConfig) validate() field.ErrorList { 151 var allErrs field.ErrorList 152 153 if err := i.validatePullSecret(); err != nil { 154 allErrs = append(allErrs, err...) 155 } 156 if err := i.validateSSHKey(); err != nil { 157 allErrs = append(allErrs, err...) 158 } 159 if err := i.validateSeedImage(); err != nil { 160 allErrs = append(allErrs, err...) 161 } 162 if err := i.validateSeedVersion(); err != nil { 163 allErrs = append(allErrs, err...) 164 } 165 if err := i.validateInstallationDisk(); err != nil { 166 allErrs = append(allErrs, err...) 167 } 168 if err := i.validateAdditionalTrustBundle(); err != nil { 169 allErrs = append(allErrs, err...) 170 } 171 if err := i.validateNetworkConfig(); err != nil { 172 allErrs = append(allErrs, err...) 173 } 174 if err := i.validateImageDigestSources(); err != nil { 175 allErrs = append(allErrs, err...) 176 } 177 if err := i.validateProxy(); err != nil { 178 allErrs = append(allErrs, err...) 179 } 180 181 return allErrs 182 } 183 184 func (i *ImageBasedInstallationConfig) validatePullSecret() field.ErrorList { 185 var allErrs field.ErrorList 186 187 pullSecretPath := field.NewPath("pullSecret") 188 189 if i.Config.PullSecret == "" { 190 allErrs = append(allErrs, field.Required(pullSecretPath, "you must specify a pullSecret")) 191 return allErrs 192 } 193 194 if err := validate.ImagePullSecret(i.Config.PullSecret); err != nil { 195 allErrs = append(allErrs, field.Invalid(pullSecretPath, i.Config.PullSecret, err.Error())) 196 } 197 198 return allErrs 199 } 200 201 func (i *ImageBasedInstallationConfig) validateSSHKey() field.ErrorList { 202 var allErrs field.ErrorList 203 204 // empty SSHKey is fine 205 if i.Config.SSHKey == "" { 206 return nil 207 } 208 209 sshKeyPath := field.NewPath("sshKey") 210 211 if err := validate.SSHPublicKey(i.Config.SSHKey); err != nil { 212 allErrs = append(allErrs, field.Invalid(sshKeyPath, i.Config.SSHKey, err.Error())) 213 } 214 215 return allErrs 216 } 217 218 func (i *ImageBasedInstallationConfig) validateAdditionalTrustBundle() field.ErrorList { 219 var allErrs field.ErrorList 220 221 // empty AdditionalTrustBundle is fine 222 if i.Config.AdditionalTrustBundle == "" { 223 return nil 224 } 225 226 additionalTrustBundlePath := field.NewPath("additionalTrustBundle") 227 228 if err := validate.CABundle(i.Config.AdditionalTrustBundle); err != nil { 229 allErrs = append(allErrs, field.Invalid(additionalTrustBundlePath, i.Config.AdditionalTrustBundle, err.Error())) 230 } 231 232 return allErrs 233 } 234 235 func (i *ImageBasedInstallationConfig) validateSeedImage() field.ErrorList { 236 var allErrs field.ErrorList 237 238 seedImagePath := field.NewPath("seedImage") 239 240 if i.Config.SeedImage == "" { 241 allErrs = append(allErrs, field.Required(seedImagePath, "you must specify a seedImage")) 242 } 243 244 return allErrs 245 } 246 247 func (i *ImageBasedInstallationConfig) validateSeedVersion() field.ErrorList { 248 var allErrs field.ErrorList 249 250 seedVersionPath := field.NewPath("seedVersion") 251 252 if i.Config.SeedVersion == "" { 253 allErrs = append(allErrs, field.Required(seedVersionPath, "you must specify a seedVersion")) 254 } 255 256 return allErrs 257 } 258 259 func (i *ImageBasedInstallationConfig) validateInstallationDisk() field.ErrorList { 260 var allErrs field.ErrorList 261 262 installationDiskPath := field.NewPath("installationDisk") 263 264 if i.Config.InstallationDisk == "" { 265 allErrs = append(allErrs, field.Required(installationDiskPath, "you must specify an installationDisk")) 266 } 267 268 return allErrs 269 } 270 271 func (i *ImageBasedInstallationConfig) validateNetworkConfig() field.ErrorList { 272 var allErrs field.ErrorList 273 274 // empty NetworkConfig is fine 275 if i.Config.NetworkConfig.String() == "" { 276 return nil 277 } 278 279 networkConfig := field.NewPath("networkConfig") 280 281 staticNetworkConfigGenerator := staticnetworkconfig.New(logrus.StandardLogger(), staticnetworkconfig.Config{MaxConcurrentGenerations: 2}) 282 283 // Validate the network config using nmstatectl. 284 if err := staticNetworkConfigGenerator.ValidateNMStateYaml(context.Background(), i.Config.NetworkConfig.String()); err != nil { 285 allErrs = append(allErrs, field.Invalid(networkConfig, i.Config.NetworkConfig, err.Error())) 286 } 287 288 return allErrs 289 } 290 291 func (i *ImageBasedInstallationConfig) validateImageDigestSources() field.ErrorList { 292 allErrs := field.ErrorList{} 293 294 fldPath := field.NewPath("imageDigestSources") 295 296 for gidx, group := range i.Config.ImageDigestSources { 297 groupf := fldPath.Index(gidx) 298 if err := validateNamedRepository(group.Source); err != nil { 299 allErrs = append(allErrs, field.Invalid(groupf.Child("source"), group.Source, err.Error())) 300 } 301 302 for midx, mirror := range group.Mirrors { 303 if err := validateNamedRepository(mirror); err != nil { 304 allErrs = append(allErrs, field.Invalid(groupf.Child("mirrors").Index(midx), mirror, err.Error())) 305 continue 306 } 307 } 308 } 309 return allErrs 310 } 311 312 func validateNamedRepository(r string) error { 313 ref, err := dockerref.ParseNamed(r) 314 if err != nil { 315 // If a mirror name is provided without the named reference, 316 // then the name is not considered canonical and will cause 317 // an error. e.g. registry.lab.redhat.com:5000 will result 318 // in an error. Instead we will check whether the input is 319 // a valid hostname as a workaround. 320 if errors.Is(err, dockerref.ErrNameNotCanonical) { 321 // If the hostname string contains a port, lets attempt 322 // to split them 323 host, _, err := net.SplitHostPort(r) 324 if err != nil { 325 host = r 326 } 327 if err = validate.Host(host); err != nil { 328 return fmt.Errorf("the repository provided is invalid: %w", err) 329 } 330 return nil 331 } 332 return fmt.Errorf("failed to parse: %w", err) 333 } 334 if !dockerref.IsNameOnly(ref) { 335 return errors.New("must be repository--not reference") 336 } 337 return nil 338 } 339 340 func (i *ImageBasedInstallationConfig) validateProxy() field.ErrorList { 341 allErrs := field.ErrorList{} 342 343 // empty Proxy is fine 344 if i.Config.Proxy == nil { 345 return nil 346 } 347 348 fldPath := field.NewPath("proxy") 349 350 if i.Config.Proxy.HTTPProxy == "" && i.Config.Proxy.HTTPSProxy == "" { 351 allErrs = append(allErrs, field.Required(fldPath, "must include httpProxy or httpsProxy")) 352 } 353 354 if i.Config.Proxy.HTTPProxy != "" { 355 allErrs = append(allErrs, validateURI(i.Config.Proxy.HTTPProxy, fldPath.Child("httpProxy"), []string{"http"})...) 356 } 357 if i.Config.Proxy.HTTPSProxy != "" { 358 allErrs = append(allErrs, validateURI(i.Config.Proxy.HTTPSProxy, fldPath.Child("httpsProxy"), []string{"http", "https"})...) 359 } 360 if i.Config.Proxy.NoProxy != "" && i.Config.Proxy.NoProxy != "*" { 361 for idx, v := range strings.Split(i.Config.Proxy.NoProxy, ",") { 362 v = strings.TrimSpace(v) 363 errDomain := validate.NoProxyDomainName(v) 364 _, _, errCIDR := net.ParseCIDR(v) 365 ip := net.ParseIP(v) 366 if errDomain != nil && errCIDR != nil && ip == nil { 367 allErrs = append(allErrs, field.Invalid(fldPath.Child("noProxy"), i.Config.Proxy.NoProxy, fmt.Sprintf( 368 "each element of noProxy must be a IP, CIDR or domain without wildcard characters, which is violated by element %d %q", idx, v))) 369 } 370 } 371 } 372 373 return allErrs 374 } 375 376 func validateURI(uri string, fldPath *field.Path, schemes []string) field.ErrorList { 377 parsed, err := url.ParseRequestURI(uri) 378 if err != nil { 379 return field.ErrorList{field.Invalid(fldPath, uri, err.Error())} 380 } 381 for _, scheme := range schemes { 382 if scheme == parsed.Scheme { 383 return nil 384 } 385 } 386 return field.ErrorList{field.NotSupported(fldPath, parsed.Scheme, schemes)} 387 }