github.com/openshift/installer@v1.4.17/pkg/asset/agent/mirror/registriesconf.go (about)

     1  package mirror
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  
    10  	"github.com/containers/image/v5/pkg/sysregistriesv2"
    11  	"github.com/pelletier/go-toml"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  
    15  	"github.com/openshift/installer/pkg/asset"
    16  	"github.com/openshift/installer/pkg/asset/agent"
    17  	"github.com/openshift/installer/pkg/asset/agent/joiner"
    18  	"github.com/openshift/installer/pkg/asset/agent/workflow"
    19  	"github.com/openshift/installer/pkg/asset/ignition/bootstrap"
    20  	"github.com/openshift/installer/pkg/asset/releaseimage"
    21  	"github.com/openshift/installer/pkg/types"
    22  )
    23  
    24  var (
    25  	// RegistriesConfFilename defines the name of the file on disk
    26  	RegistriesConfFilename = filepath.Join(mirrorConfigDir, "registries.conf")
    27  )
    28  
    29  // The default registries.conf file is the podman default as it appears in
    30  // CoreOS, with no unqualified-search-registries.
    31  const defaultRegistriesConf = `
    32  # NOTE: RISK OF USING UNQUALIFIED IMAGE NAMES
    33  # We recommend always using fully qualified image names including the registry
    34  # server (full dns name), namespace, image name, and tag
    35  # (e.g., registry.redhat.io/ubi8/ubi:latest). Pulling by digest (i.e.,
    36  # quay.io/repository/name@digest) further eliminates the ambiguity of tags.
    37  # When using short names, there is always an inherent risk that the image being
    38  # pulled could be spoofed. For example, a user wants to pull an image named
    39  # 'foobar' from a registry and expects it to come from myregistry.com. If
    40  # myregistry.com is not first in the search list, an attacker could place a
    41  # different 'foobar' image at a registry earlier in the search list. The user
    42  # would accidentally pull and run the attacker's image and code rather than the
    43  # intended content. We recommend only adding registries which are completely
    44  # trusted (i.e., registries which don't allow unknown or anonymous users to
    45  # create accounts with arbitrary names). This will prevent an image from being
    46  # spoofed, squatted or otherwise made insecure.  If it is necessary to use one
    47  # of these registries, it should be added at the end of the list.
    48  #
    49  # # An array of host[:port] registries to try when pulling an unqualified image, in order.
    50  
    51  unqualified-search-registries = []
    52  
    53  # [[registry]]
    54  # # The "prefix" field is used to choose the relevant [[registry]] TOML table;
    55  # # (only) the TOML table with the longest match for the input image name
    56  # # (taking into account namespace/repo/tag/digest separators) is used.
    57  # # 
    58  # # The prefix can also be of the form: *.example.com for wildcard subdomain
    59  # # matching.
    60  # #
    61  # # If the prefix field is missing, it defaults to be the same as the "location" field.
    62  # prefix = "example.com/foo"
    63  #
    64  # # If true, unencrypted HTTP as well as TLS connections with untrusted
    65  # # certificates are allowed.
    66  # insecure = false
    67  #
    68  # # If true, pulling images with matching names is forbidden.
    69  # blocked = false
    70  #
    71  # # The physical location of the "prefix"-rooted namespace.
    72  # #
    73  # # By default, this is equal to "prefix" (in which case "prefix" can be omitted
    74  # # and the [[registry]] TOML table can only specify "location").
    75  # #
    76  # # Example: Given
    77  # #   prefix = "example.com/foo"
    78  # #   location = "internal-registry-for-example.net/bar"
    79  # # requests for the image example.com/foo/myimage:latest will actually work with the
    80  # # internal-registry-for-example.net/bar/myimage:latest image.
    81  #
    82  # # The location can be empty iff prefix is in a
    83  # # wildcarded format: "*.example.com". In this case, the input reference will
    84  # # be used as-is without any rewrite.
    85  # location = internal-registry-for-example.com/bar"
    86  #
    87  # # (Possibly-partial) mirrors for the "prefix"-rooted namespace.
    88  # #
    89  # # The mirrors are attempted in the specified order; the first one that can be
    90  # # contacted and contains the image will be used (and if none of the mirrors contains the image,
    91  # # the primary location specified by the "registry.location" field, or using the unmodified
    92  # # user-specified reference, is tried last).
    93  # #
    94  # # Each TOML table in the "mirror" array can contain the following fields, with the same semantics
    95  # # as if specified in the [[registry]] TOML table directly:
    96  # # - location
    97  # # - insecure
    98  # [[registry.mirror]]
    99  # location = "example-mirror-0.local/mirror-for-foo"
   100  # [[registry.mirror]]
   101  # location = "example-mirror-1.local/mirrors/foo"
   102  # insecure = true
   103  # # Given the above, a pull of example.com/foo/image:latest will try:
   104  # # 1. example-mirror-0.local/mirror-for-foo/image:latest
   105  # # 2. example-mirror-1.local/mirrors/foo/image:latest
   106  # # 3. internal-registry-for-example.net/bar/image:latest
   107  # # in order, and use the first one that exists.
   108  `
   109  
   110  // RegistriesConf generates the registries.conf file.
   111  type RegistriesConf struct {
   112  	File         *asset.File
   113  	Config       *sysregistriesv2.V2RegistriesConf
   114  	MirrorConfig []RegistriesConfig
   115  }
   116  
   117  // RegistriesConfig holds the data extracted from registries.conf
   118  type RegistriesConfig struct {
   119  	Location string
   120  	Mirror   string
   121  }
   122  
   123  var _ asset.WritableAsset = (*RegistriesConf)(nil)
   124  
   125  // Name returns a human friendly name for the asset.
   126  func (*RegistriesConf) Name() string {
   127  	return "Mirror Registries Config"
   128  }
   129  
   130  // Dependencies returns all of the dependencies directly needed to generate
   131  // the asset.
   132  func (*RegistriesConf) Dependencies() []asset.Asset {
   133  	return []asset.Asset{
   134  		&workflow.AgentWorkflow{},
   135  		&joiner.ClusterInfo{},
   136  		&agent.OptionalInstallConfig{},
   137  		&releaseimage.Image{},
   138  	}
   139  }
   140  
   141  // Generate generates the registries.conf file from install-config.
   142  func (i *RegistriesConf) Generate(_ context.Context, dependencies asset.Parents) error {
   143  	agentWorkflow := &workflow.AgentWorkflow{}
   144  	clusterInfo := &joiner.ClusterInfo{}
   145  	installConfig := &agent.OptionalInstallConfig{}
   146  	releaseImage := &releaseimage.Image{}
   147  	dependencies.Get(installConfig, releaseImage, agentWorkflow, clusterInfo)
   148  
   149  	var imageDigestSources []types.ImageDigestSource
   150  	var deprecatedImageContentSources []types.ImageContentSource
   151  	var image string
   152  
   153  	switch agentWorkflow.Workflow {
   154  	case workflow.AgentWorkflowTypeInstall:
   155  		if installConfig.Supplied {
   156  			imageDigestSources = installConfig.Config.ImageDigestSources
   157  			deprecatedImageContentSources = installConfig.Config.DeprecatedImageContentSources
   158  		}
   159  		image = releaseImage.PullSpec
   160  
   161  	case workflow.AgentWorkflowTypeAddNodes:
   162  		imageDigestSources = clusterInfo.ImageDigestSources
   163  		deprecatedImageContentSources = clusterInfo.DeprecatedImageContentSources
   164  		image = clusterInfo.ReleaseImage
   165  
   166  	default:
   167  		return fmt.Errorf("AgentWorkflowType value not supported: %s", agentWorkflow.Workflow)
   168  	}
   169  
   170  	if len(deprecatedImageContentSources) == 0 && len(imageDigestSources) == 0 {
   171  		return i.generateDefaultRegistriesConf()
   172  	}
   173  
   174  	err := i.generateRegistriesConf(imageDigestSources, deprecatedImageContentSources)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	if !i.releaseImageIsSameInRegistriesConf(image) {
   180  		logrus.Warnf(fmt.Sprintf("The imageDigestSources configuration in install-config.yaml should have at least one source field matching the releaseImage value %s", releaseImage.PullSpec))
   181  	}
   182  
   183  	registriesData, err := toml.Marshal(i.Config)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	i.File = &asset.File{
   189  		Filename: RegistriesConfFilename,
   190  		Data:     registriesData,
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func (i *RegistriesConf) generateRegistriesConf(imageDigestSources []types.ImageDigestSource, deprecatedImageContentSources []types.ImageContentSource) error {
   197  	if len(deprecatedImageContentSources) != 0 && len(imageDigestSources) != 0 {
   198  		return fmt.Errorf("invalid install-config.yaml, cannot set imageContentSources and imageDigestSources at the same time")
   199  	}
   200  
   201  	digestMirrorSources := []types.ImageDigestSource{}
   202  	if len(deprecatedImageContentSources) > 0 {
   203  		digestMirrorSources = bootstrap.ContentSourceToDigestMirror(deprecatedImageContentSources)
   204  	} else if len(imageDigestSources) > 0 {
   205  		digestMirrorSources = append(digestMirrorSources, imageDigestSources...)
   206  	}
   207  
   208  	registries := &sysregistriesv2.V2RegistriesConf{
   209  		Registries: []sysregistriesv2.Registry{},
   210  	}
   211  	for _, group := range bootstrap.MergedMirrorSets(digestMirrorSources) {
   212  		if len(group.Mirrors) == 0 {
   213  			continue
   214  		}
   215  
   216  		registry := sysregistriesv2.Registry{}
   217  		registry.Endpoint.Location = group.Source
   218  		registry.MirrorByDigestOnly = true
   219  		for _, mirror := range group.Mirrors {
   220  			registry.Mirrors = append(registry.Mirrors, sysregistriesv2.Endpoint{Location: mirror})
   221  		}
   222  		registries.Registries = append(registries.Registries, registry)
   223  	}
   224  	i.Config = registries
   225  	i.setMirrorConfig(i.Config)
   226  
   227  	return nil
   228  }
   229  
   230  // Files returns the files generated by the asset.
   231  func (i *RegistriesConf) Files() []*asset.File {
   232  	if i.File != nil {
   233  		return []*asset.File{i.File}
   234  	}
   235  	return []*asset.File{}
   236  }
   237  
   238  // Load returns RegistriesConf asset from the disk.
   239  func (i *RegistriesConf) Load(f asset.FileFetcher) (bool, error) {
   240  	ctx := context.TODO()
   241  
   242  	releaseImage := &releaseimage.Image{}
   243  	if err := releaseImage.Generate(ctx, asset.Parents{}); err != nil {
   244  		return false, fmt.Errorf("failed to generate the release image asset: %w", err)
   245  	}
   246  
   247  	file, err := f.FetchByName(RegistriesConfFilename)
   248  	if err != nil {
   249  		if os.IsNotExist(err) {
   250  			return false, nil
   251  		}
   252  		return false, errors.Wrap(err, fmt.Sprintf("failed to load %s file", RegistriesConfFilename))
   253  	}
   254  
   255  	registriesConf := &sysregistriesv2.V2RegistriesConf{}
   256  	if err := toml.Unmarshal(file.Data, registriesConf); err != nil {
   257  		return false, errors.Wrapf(err, "failed to unmarshal %s", RegistriesConfFilename)
   258  	}
   259  
   260  	i.File, i.Config = file, registriesConf
   261  	i.setMirrorConfig(i.Config)
   262  
   263  	if string(i.File.Data) != defaultRegistriesConf {
   264  		if i.validateRegistriesConf() {
   265  			if !i.releaseImageIsSameInRegistriesConf(releaseImage.PullSpec) {
   266  				logrus.Warnf(fmt.Sprintf("%s should have an entry matching the releaseImage %s", RegistriesConfFilename, releaseImage.PullSpec))
   267  			}
   268  		}
   269  	}
   270  
   271  	return true, nil
   272  }
   273  
   274  func (i *RegistriesConf) validateRegistriesConf() bool {
   275  	for _, registry := range i.Config.Registries {
   276  		if registry.Endpoint.Location == "" {
   277  			logrus.Warnf(fmt.Sprintf("Location key not found in %s", RegistriesConfFilename))
   278  			return false
   279  		}
   280  	}
   281  	return true
   282  }
   283  
   284  func (i *RegistriesConf) releaseImageIsSameInRegistriesConf(releaseImage string) bool {
   285  	return GetMirrorFromRelease(releaseImage, i) != ""
   286  }
   287  
   288  func (i *RegistriesConf) generateDefaultRegistriesConf() error {
   289  	i.File = &asset.File{
   290  		Filename: RegistriesConfFilename,
   291  		Data:     []byte(defaultRegistriesConf),
   292  	}
   293  	registriesConf := &sysregistriesv2.V2RegistriesConf{}
   294  	if err := toml.Unmarshal([]byte(defaultRegistriesConf), registriesConf); err != nil {
   295  		return errors.Wrapf(err, "failed to unmarshal %s", RegistriesConfFilename)
   296  	}
   297  	i.Config = registriesConf
   298  	return nil
   299  }
   300  
   301  func (i *RegistriesConf) setMirrorConfig(registriesConf *sysregistriesv2.V2RegistriesConf) {
   302  	mirrorConfig := make([]RegistriesConfig, len(registriesConf.Registries))
   303  	for i, reg := range registriesConf.Registries {
   304  		mirrorConfig[i] = RegistriesConfig{
   305  			Location: reg.Location,
   306  			Mirror:   reg.Mirrors[0].Location,
   307  		}
   308  	}
   309  	i.MirrorConfig = mirrorConfig
   310  }
   311  
   312  // GetMirrorFromRelease gets the matching mirror configured for the releaseImage.
   313  func GetMirrorFromRelease(releaseImage string, registriesConfig *RegistriesConf) string {
   314  	source := regexp.MustCompile(`^(.+?)(@sha256)?:(.+)`).FindStringSubmatch(releaseImage)
   315  	for _, config := range registriesConfig.MirrorConfig {
   316  		if config.Location == source[1] {
   317  			// include the tag with the build release image
   318  			switch len(source) {
   319  			case 4:
   320  				// Has Sha256
   321  				return fmt.Sprintf("%s%s:%s", config.Mirror, source[2], source[3])
   322  			case 3:
   323  				return fmt.Sprintf("%s:%s", config.Mirror, source[2])
   324  			}
   325  		}
   326  	}
   327  
   328  	return ""
   329  }