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  }