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 }