github.com/verrazzano/verrazzano@v1.7.0/pkg/bom/bom.go (about)

     1  // Copyright (c) 2021, 2023, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package bom
     5  
     6  import (
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"strings"
    12  
    13  	"github.com/verrazzano/verrazzano/platform-operator/constants"
    14  )
    15  
    16  const defaultImageKey = "image"
    17  const slash = "/"
    18  const tagSep = ":"
    19  
    20  // Bom contains information related to the bill of materials along with structures to process it.
    21  // The bom file is verrazzano-bom.json and it mainly has image information.
    22  type Bom struct {
    23  	// bomDoc contains the contents of the JSON bom, in go structure format.
    24  	bomDoc BomDoc
    25  
    26  	// subComponentMap is a map of subcomponents keyed by subcomponent name.
    27  	subComponentMap map[string]*BomSubComponent
    28  }
    29  
    30  // BomDoc contains product metadata for components installed by Verrazzano.
    31  // Currently, this metadata only describes images and image repositories.
    32  // This information is needed by the helm charts and used during install/upgrade.
    33  type BomDoc struct {
    34  	// Registry is the top level registry URI which contains the image repositories.
    35  	// An example is ghcr.io
    36  	Registry string `json:"registry"`
    37  
    38  	// Version is the verrazzano version corresponding to the build
    39  	Version string `json:"version"`
    40  
    41  	// Components is the array of component boms
    42  	Components []BomComponent `json:"components"`
    43  
    44  	// SupportedKubernetesVersions is the array of supported Kubernetes versions
    45  	SupportedKubernetesVersions []string `json:"supportedKubernetesVersions"`
    46  }
    47  
    48  // BomComponent represents a high level component, such as Istio.
    49  // Each component has one or more subcomponents.
    50  type BomComponent struct {
    51  	// The name of the component, for example: Istio
    52  	Name string `json:"name"`
    53  	// Version of the component
    54  	Version string `json:"version,omitempty"`
    55  	// SubComponents is the array of subcomponents in the component
    56  	SubComponents []BomSubComponent `json:"subcomponents"`
    57  }
    58  
    59  // BomSubComponent contains the bom information for a single helm chart.
    60  // Istio is an example of a component with several subcomponents.
    61  type BomSubComponent struct {
    62  	Name string `json:"name"`
    63  
    64  	// Repository is the name of the repository within a registry.  This is combined
    65  	// with the registry Value to form the image URL prefix, for example: ghcr.io/verrazzano,
    66  	// where ghci.io is the registry and Verrazzano is the repository name.
    67  	Repository string `json:"repository"`
    68  
    69  	// Override the registry within a subcomponent
    70  	Registry string `json:"registry"`
    71  
    72  	// Images is the array of images for this subcomponent
    73  	Images []BomImage `json:"images"`
    74  }
    75  
    76  // BomImage describes a single image used by one of the helm charts.  This structure
    77  // is needed to render the helm chart manifest, so the image fields are correctly populated
    78  // in the resulting YAML.  There is no helm standard which specifies the keys for
    79  // image information used by a template, each product usually has a custom way to do this.
    80  // The helm keys fields in this structure specify those custom keys.
    81  type BomImage struct {
    82  	// ImageName specifies the name of the image tag, such as `nginx-ingress-controller`
    83  	ImageName string `json:"image"`
    84  
    85  	// ImageTag specifies the name of the image tag, such as `0.46.0-20210510134749-abc2d2088`
    86  	ImageTag string `json:"tag"`
    87  
    88  	// Registry is the image registry. It can be used to override the subcomponent registry
    89  	Registry string `json:"registry,omitempty"`
    90  
    91  	// Repository is the image repository. It can be used to override the subcomponent repository
    92  	Repository string `json:"repository,omitempty"`
    93  
    94  	// HelmRegistryKey is the helm template Key which identifies the registry for an image.  An example is
    95  	// `image.registry` in external-dns.  The default is empty string.
    96  	HelmRegistryKey string `json:"helmRegKey"`
    97  
    98  	// HelmRepoKey is the helm template Key which stores the value of the repository for an image.
    99  	HelmRepoKey string `json:"helmRepoKey"`
   100  
   101  	// HelmImageKey is the helm template Key which identifies the base image name, without the registry or parent repo
   102  	// parts of the path.  For example, if the full image name is myreg.io/foo/bar/myimage:v1.0, the value of this key
   103  	// will be "myimage".  See the Istio proxyv2 entry in the BOM file for an example.
   104  	HelmImageKey string `json:"helmImageKey"`
   105  
   106  	// HelmTagKey is the helm template Key which stores the value of the image tag.  For example,
   107  	// if the full image name is myreg.io/foo/bar/myimage:v1.0, the value of this key will be "v1.0"
   108  	HelmTagKey string `json:"helmTagKey"`
   109  
   110  	// HelmFullImageKey is the helm path Key which identifies the image name without the registry or tag.  For example,
   111  	// if the full image name is myreg.io/foo/bar/myimage:v1.0, the value of this key will be
   112  	// "foo/bar/myimage".
   113  	HelmFullImageKey string `json:"helmFullImageKey"`
   114  
   115  	// HelmRegistryAndRepoKey is a helm Key which stores the registry and repo parts of the image path.  For example,
   116  	// if the full image name is myreg.io/foo/bar/myimage:v1.0 the value of this key will be "myreg.io/foo/bar".
   117  	// See `image.repository` in the external-dns component
   118  	HelmRegistryAndRepoKey string `json:"helmRegistryAndRepoKey"`
   119  }
   120  
   121  // keyVal defines the Key, Value pair used to override a single helm Value
   122  type KeyValue struct {
   123  	Key       string
   124  	Value     string
   125  	SetString bool // for --set-string
   126  	SetFile   bool // for --set-file
   127  	IsFile    bool // for -f
   128  }
   129  
   130  // Create a new BOM instance from a JSON file
   131  func NewBom(bomPath string) (Bom, error) {
   132  	jsonBom, err := os.ReadFile(bomPath)
   133  	if err != nil {
   134  		return Bom{}, err
   135  	}
   136  	return NewBOMFromJSON(jsonBom)
   137  }
   138  
   139  // NewBOMFromJSON Create a new BOM instance from a JSON payload
   140  func NewBOMFromJSON(jsonBom []byte) (Bom, error) {
   141  	bom := Bom{
   142  		subComponentMap: make(map[string]*BomSubComponent),
   143  	}
   144  	err := bom.init(string(jsonBom))
   145  	if err != nil {
   146  		return Bom{}, err
   147  	}
   148  	return bom, nil
   149  }
   150  
   151  // Initialize the BomInfo.  Load the Bom from the JSON file and build
   152  // a map of subcomponents
   153  func (b *Bom) init(jsonBom string) error {
   154  	// Convert the json into a to bom
   155  	if err := json.Unmarshal([]byte(jsonBom), &b.bomDoc); err != nil {
   156  		return err
   157  	}
   158  
   159  	// Build a map of subcomponents
   160  	for _, comp := range b.bomDoc.Components {
   161  		for i, sub := range comp.SubComponents {
   162  			b.subComponentMap[sub.Name] = &comp.SubComponents[i]
   163  		}
   164  	}
   165  	return nil
   166  }
   167  
   168  // GetRegistry Gets the registry name
   169  func (b *Bom) GetRegistry() string {
   170  	return b.bomDoc.Registry
   171  }
   172  
   173  // GetVersion gets the BOM product version
   174  func (b *Bom) GetVersion() string {
   175  	return b.bomDoc.Version
   176  }
   177  
   178  // GetComponent gets the BOM component
   179  func (b *Bom) GetComponent(componentName string) (*BomComponent, error) {
   180  	for _, comp := range b.bomDoc.Components {
   181  		if comp.Name == componentName {
   182  			return &comp, nil
   183  		}
   184  	}
   185  	return nil, errors.New("unknown component " + componentName)
   186  }
   187  
   188  func (b *Bom) GetComponentVersion(componentName string) (string, error) {
   189  	component, err := b.GetComponent(componentName)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	if len(component.Version) == 0 {
   194  		return "", fmt.Errorf("Did not find valid version for component %s: %s", component, component.Version)
   195  	}
   196  	return component.Version, nil
   197  }
   198  
   199  // GetSubcomponent gets the bom subcomponent
   200  func (b *Bom) GetSubcomponent(subComponentName string) (*BomSubComponent, error) {
   201  	sc, ok := b.subComponentMap[subComponentName]
   202  	if !ok {
   203  		return nil, errors.New("unknown subcomponent " + subComponentName)
   204  	}
   205  	return sc, nil
   206  }
   207  
   208  // GetSubcomponentImages the imageBoms for a subcomponent
   209  func (b *Bom) GetSubcomponentImages(subComponentName string) ([]BomImage, error) {
   210  	sc, err := b.GetSubcomponent(subComponentName)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	return sc.Images, nil
   215  }
   216  
   217  func (b *Bom) FindImage(subcomponentName, imageName string) (BomImage, error) {
   218  	images, err := b.GetSubcomponentImages(subcomponentName)
   219  	if err != nil {
   220  		return BomImage{}, err
   221  	}
   222  	for _, image := range images {
   223  		if image.ImageName == imageName {
   224  			return image, nil
   225  		}
   226  	}
   227  	return BomImage{}, fmt.Errorf("Image %s not found for sub-component %s", imageName, subcomponentName)
   228  }
   229  
   230  // GetSubcomponentImageCount returns the number of subcomponent images
   231  func (b *Bom) GetSubcomponentImageCount(subComponentName string) int {
   232  	imageBom, ok := b.subComponentMap[subComponentName]
   233  	if !ok {
   234  		return 0
   235  	}
   236  	return len(imageBom.Images)
   237  }
   238  
   239  // BuildImageOverrides builds the image overrides array for the subComponent.
   240  // Each override has an array of 1-n Helm Key:Value pairs
   241  func (b *Bom) BuildImageOverrides(subComponentName string) ([]KeyValue, error) {
   242  	kvs, _, err := b.BuildImageStrings(subComponentName)
   243  	return kvs, err
   244  }
   245  
   246  // GetImageNameList build the image names and return a list of image names
   247  func (b *Bom) GetImageNameList(subComponentName string) ([]string, error) {
   248  	_, images, err := b.BuildImageStrings(subComponentName)
   249  	return images, err
   250  }
   251  
   252  // BuildImageStrings builds the image overrides array for the subComponent.
   253  // Each override has an array of 1-n Helm Key:Value pairs
   254  // Also return the set of fully constructed image names
   255  func (b *Bom) BuildImageStrings(subComponentName string) ([]KeyValue, []string, error) {
   256  	var fullImageNames []string
   257  	sc, ok := b.subComponentMap[subComponentName]
   258  	if !ok {
   259  		return nil, nil, errors.New("unknown subcomponent " + subComponentName)
   260  	}
   261  
   262  	// Loop through the images used by this subcomponent, building
   263  	// the list of Key:Value pairs.  At the very least, this will build
   264  	// a single Value for the fully qualified image name in the format of
   265  	// registry/repository/image.tag
   266  	var kvs []KeyValue
   267  	for _, imageBom := range sc.Images {
   268  		partialImageNameBldr := strings.Builder{}
   269  		registry := b.ResolveRegistry(sc, imageBom)
   270  		repo := b.ResolveRepo(sc, imageBom)
   271  
   272  		// Normally, the registry is the first segment of the image name, for example "ghcr.io/"
   273  		// However, there are exceptions like in external-dns, where the registry is a separate helm field,
   274  		// in which case the registry is omitted from the image full name.
   275  		if imageBom.HelmRegistryKey != "" {
   276  			kvs = append(kvs, KeyValue{
   277  				Key:   imageBom.HelmRegistryKey,
   278  				Value: registry,
   279  			})
   280  		} else {
   281  			partialImageNameBldr.WriteString(registry)
   282  			partialImageNameBldr.WriteString(slash)
   283  		}
   284  
   285  		// Either write the repo name Key Value, or append it to the full image path
   286  		if imageBom.HelmRepoKey != "" {
   287  			kvs = append(kvs, KeyValue{
   288  				Key:   imageBom.HelmRepoKey,
   289  				Value: repo,
   290  			})
   291  		} else {
   292  			if len(repo) > 0 {
   293  				partialImageNameBldr.WriteString(repo)
   294  				partialImageNameBldr.WriteString(slash)
   295  			}
   296  		}
   297  
   298  		// If the Registry/Repo key is defined then set it
   299  		if imageBom.HelmRegistryAndRepoKey != "" {
   300  			regAndRep := registry + "/" + repo
   301  			kvs = append(kvs, KeyValue{
   302  				Key:   imageBom.HelmRegistryAndRepoKey,
   303  				Value: regAndRep,
   304  			})
   305  		}
   306  
   307  		// Either write the image name Key Value, or append it to the full image path
   308  		if imageBom.HelmImageKey != "" {
   309  			kvs = append(kvs, KeyValue{
   310  				Key:   imageBom.HelmImageKey,
   311  				Value: imageBom.ImageName,
   312  			})
   313  		} else {
   314  			partialImageNameBldr.WriteString(imageBom.ImageName)
   315  		}
   316  
   317  		// Either write the tag name Key Value, or append it to the full image path
   318  		if imageBom.HelmTagKey != "" {
   319  			kvs = append(kvs, KeyValue{
   320  				Key:   imageBom.HelmTagKey,
   321  				Value: imageBom.ImageTag,
   322  			})
   323  		} else {
   324  			partialImageNameBldr.WriteString(tagSep)
   325  			partialImageNameBldr.WriteString(imageBom.ImageTag)
   326  		}
   327  
   328  		// This partial image path may be a subset of the full image name or the entire image path
   329  		partialImagePath := partialImageNameBldr.String()
   330  
   331  		// If the image path Key is present the create the kv with the partial image path
   332  		if imageBom.HelmFullImageKey != "" {
   333  			kvs = append(kvs, KeyValue{
   334  				Key:   imageBom.HelmFullImageKey,
   335  				Value: partialImagePath,
   336  			})
   337  		}
   338  		// Default the image Key if there are no specified tags.  Keycloak theme needs this
   339  		if len(kvs) == 0 {
   340  			kvs = append(kvs, KeyValue{
   341  				Key:   defaultImageKey,
   342  				Value: partialImagePath,
   343  			})
   344  		}
   345  		// Add the full image name to the list
   346  		fullImageName := fmt.Sprintf("%s/%s/%s:%s", registry, repo, imageBom.ImageName, imageBom.ImageTag)
   347  		fullImageNames = append(fullImageNames, fullImageName)
   348  	}
   349  	return kvs, fullImageNames, nil
   350  }
   351  
   352  // ResolveRegistry resolves the registry name using the ENV var if it exists.
   353  func (b *Bom) ResolveRegistry(sc *BomSubComponent, img BomImage) string {
   354  	// Get the registry ENV override, if it doesn't exist use the default
   355  	registry := os.Getenv(constants.RegistryOverrideEnvVar)
   356  	if registry == "" {
   357  		registry = b.bomDoc.Registry
   358  		if len(sc.Registry) > 0 {
   359  			registry = sc.Registry
   360  		}
   361  		if len(img.Registry) > 0 {
   362  			registry = img.Registry
   363  		}
   364  	}
   365  	return registry
   366  }
   367  
   368  // ResolveRepo resolves the repository name using the ENV var if it exists.
   369  func (b *Bom) ResolveRepo(sc *BomSubComponent, img BomImage) string {
   370  	// Get the repo ENV override.  This needs to get prepended to the bom repo
   371  	userRepo := os.Getenv(constants.ImageRepoOverrideEnvVar)
   372  	repo := sc.Repository
   373  	if len(img.Repository) > 0 {
   374  		repo = img.Repository
   375  	}
   376  
   377  	if userRepo != "" {
   378  		repo = userRepo + slash + repo
   379  	}
   380  	return repo
   381  }
   382  
   383  // FindKV searches an array of KeyValue structs for a Key and returns the Value if found, or returns an empty string
   384  func FindKV(kvs []KeyValue, key string) string {
   385  	for _, kv := range kvs {
   386  		if kv.Key == key {
   387  			return kv.Value
   388  		}
   389  	}
   390  	return ""
   391  }
   392  
   393  // GetSupportedKubernetesVersion gets supported Kubernetes versions
   394  func (b *Bom) GetSupportedKubernetesVersion() []string {
   395  	return b.bomDoc.SupportedKubernetesVersions
   396  }