Repository: Azure/ARO-RP
Pull request #1650 (2021-08-02 21:23:05)

save ocp versions to storage account
### Which issue this PR addresses:

Fixes [10486354](https://msazure.visualstudio.com/AzureRedHatOpenShift/_workitems/edit/10486354/)

### What this PR does / why we need it:

Currently, when using our upgrade script, we have to manually type in the streams from `pkg/util/version/const.go` for the commit of the currently deployed RP. This is tedious and error prone. It would be better to host this data in the same way as we do the currently deployed RP commit.

This PR adds a new container (in the same storage account as the RP commit) which returns a JSON list of the current OCP streams for a location.

Once this is merged, we can simplify our upgrade script.

### Test plan for issue:

Manually ran the code for generating the JSON, verified that it is correct.

### Is there any documentation that needs to be updated for this PR?

No


Patch:
diff --git a/deploy/rp-production-global.json b/deploy/rp-production-global.json
index 9fc8146af..e00b01a7f 100644
--- a/deploy/rp-production-global.json
+++ b/deploy/rp-production-global.json
@@ -100,6 +100,18 @@
             "dependsOn": [
                 "[resourceId('Microsoft.Storage/storageAccounts', parameters('rpVersionStorageAccountName'))]"
             ]
+        },
+        {
+            "properties": {
+                "publicAccess": "Container",
+                "metadata": null
+            },
+            "name": "[concat(parameters('rpVersionStorageAccountName'), '/default/ocpversions')]",
+            "type": "Microsoft.Storage/storageAccounts/blobServices/containers",
+            "apiVersion": "2019-04-01",
+            "dependsOn": [
+                "[resourceId('Microsoft.Storage/storageAccounts', parameters('rpVersionStorageAccountName'))]"
+            ]
         }
     ]
 }
diff --git a/pkg/deploy/bindata.go b/pkg/deploy/bindata.go
index 7fd93f48e..22c5e7889 100644
--- a/pkg/deploy/bindata.go
+++ b/pkg/deploy/bindata.go
@@ -251,7 +251,7 @@ func rpProductionGlobalSubscriptionJson() (*asset, error) {
 	return a, nil
 }
 
-var _rpProductionGlobalJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x58\x6d\x6f\xdb\x36\x10\xfe\x9e\x5f\x21\x68\x03\x64\x03\x96\x25\xf9\x2d\x76\xbe\x79\x2b\x36\x14\xe8\x1a\xc3\x09\xfa\x61\x41\x31\x50\xe4\xd9\xe1\x42\x91\xda\x91\x72\x96\x16\xf9\xef\x83\x5e\xec\x5a\x2f\x96\xbd\x34\x19\xba\x22\x08\x20\x28\xe4\xe9\xf8\xdc\xdd\x73\x0f\x49\x7f\x3e\xb3\x2c\xcb\xb2\x7f\xd4\xf4\x16\x22\x62\x5f\x58\xf6\xad\x31\xb1\xbe\xf0\xbc\x7c\xa4\x1f\x11\x49\xd6\x10\x81\x34\x7d\xf2\x29\x41\xe8\x53\x15\x15\x73\xda\x1b\xf8\xc1\xd8\xf5\x03\xd7\x0f\x3c\x06\xb1\x50\x0f\xa9\xdd\x35\x44\xb1\x20\x06\xfa\x7f\x6a\x25\x7f\xb0\x7b\xf9\x0a\x54\x49\x03\xd2\x7c\x00\xd4\x5c\xc9\x74\xa1\xa0\xef\xa7\x7f\x5b\x83\x98\x20\x89\xc0\x00\x6a\xfb\xc2\xca\x61\x65\xe3\x84\xe2\x3b\x45\x89\xe1\x4a\x5e\x6e\x00\x91\x33\x28\x19\x64\x46\xe6\x21\x4e\x47\x6d\x6d\x90\xcb\x75\xe1\x72\x37\xcb\x60\x45\x12\x61\x3e\x10\x91\x64\x56\xf6\x6e\xfa\xb1\x57\x5a\x68\x09\x5a\x25\x48\xe1\x2d\x3b\xba\x44\xa3\x0b\x2a\x12\x6d\x00\x17\x04\x41\x9a\x37\x2a\x22\x5c\xbe\x27\xd1\x71\xbc\x8d\xce\x56\xf1\x15\xe0\x86\x53\x58\x20\x97\x94\xc7\x44\x3c\x15\x16\xc6\xcf\x83\x08\x9f\x11\x51\x41\x84\x2b\xa3\x90\xac\x61\x4e\xa9\x4a\xa4\xf9\x97\xd0\xce\xf6\xdc\xda\x58\xd4\x2e\xa5\xcf\xcd\xce\xa6\xe2\x4a\xdf\x25\x35\xff\xd9\x84\xcc\x57\xb6\x17\x08\x11\x4f\x22\xbb\x64\xf1\x58\xe1\xd3\xd6\xf8\x46\x27\x61\x0e\xaa\xf3\x85\xbd\x1d\xa7\x44\x24\xa7\xdb\xb3\x08\x63\x1d\x41\xb4\x79\x2b\x19\xfc\x7d\xb9\x6a\x37\x76\xbc\xf4\x19\x74\xbb\x1f\xab\x34\xde\xe6\xe1\x37\x4e\x51\x69\xb5\x32\xfd\x9f\x95\x34\x84\x4b\xc0\x25\xac\xb9\x36\xf8\xe0\x61\xfe\xc2\x41\x57\xbf\x16\x45\x13\x65\xc0\xf9\xaa\x03\x7f\x25\x44\xe8\x2a\x96\x6a\xab\x65\x88\xd2\xc7\x36\xbd\xbf\xa2\x4a\xe2\x4e\xb7\xbf\x75\xd7\xb3\x8e\x7a\xa8\x47\x42\x62\xbe\x27\x04\x03\x7f\xe0\xbb\x41\xaa\x22\x6e\x8c\xb0\xe1\x70\xdf\x48\x9a\x4a\x29\x63\x54\x31\xa0\xe1\x59\xc1\x3f\x1f\x2c\xd1\x3e\xba\x7a\x1f\x38\xa7\x64\xf9\x3d\x98\x7b\x85\x77\x1e\x93\xfa\x77\x25\xdb\x33\xbb\x16\x2a\x24\xe2\x58\xbc\xc1\xd4\xf5\x53\xe1\x7c\xa1\x38\x0f\xc8\xd0\xff\x20\xd8\x5d\x38\x54\x49\x4a\x4c\xe7\x45\x1b\x2c\x7b\x4d\x1f\x5f\xa2\x9f\x27\xe6\x56\x21\xff\x94\x85\x98\x4e\xae\x13\xce\x3a\x05\x98\x56\xaf\x65\x9e\xd5\x75\x32\x5b\x79\xb9\xb0\x3c\x6b\x4e\x71\x91\x08\xe1\x74\xbf\xba\xc5\xbd\x18\xd5\x86\x33\x40\xed\xa1\x12\x30\xd7\x9a\xaf\x65\xba\x05\xd7\xaa\x56\x26\x51\x5d\xfe\x34\x55\xf9\xc2\x37\xb8\x8b\xaa\xe3\x9c\x86\xc2\xe9\x59\x2f\x5a\xa6\x5a\x96\x32\xc4\x69\xc0\x6f\x60\xc5\x25\x4f\x4b\x95\xed\x43\xb9\x1e\x53\xe4\x71\x3a\xb4\x6c\x0c\xa4\x5c\xe0\xb2\x93\x34\x12\xe7\x7c\x35\x1b\x07\x8c\x11\x77\x04\x6c\xe8\x8e\x26\x53\xdf\x25\xe7\x94\xb8\xa3\xe1\x0a\x82\xf3\x01\x1b\x0f\xa7\xac\xde\x48\x45\x92\xf7\x77\xc5\xaa\xf4\x34\x51\xa2\xdd\xcd\x75\x41\x86\xea\x97\xed\x9b\x53\x53\xfb\xcd\xf6\xb5\xb5\x76\x38\x8a\x41\x32\x7d\x29\x4b\xfb\xe6\x6e\xfa\xdb\x24\x44\x09\xe7\xc7\xef\x5a\x58\x9c\x5f\x32\xd1\x58\x5e\x5a\x9b\x91\x55\x4b\xbb\x75\xad\xee\x40\x66\xe3\xc8\xc3\xc4\x28\x7c\xd5\x95\x6f\x52\x57\x46\xd3\xd9\x74\x38\x1e\x8e\xdc\x21\xf3\x27\xee\x88\xd1\xd0\x25\xe3\xc9\xc4\xf5\xa7\x64\x32\x1b\x41\x18\x0c\xce\x67\x4f\xd0\x95\xa6\x4b\xc2\xab\xae\xfc\x77\xba\x72\xf4\x42\x71\x65\x88\x64\x04\xd9\x1f\xef\x96\x57\xed\x09\x3e\xd6\x4b\x44\x08\x75\xff\x93\x50\xe1\x22\x09\x05\xa7\x73\x4a\x41\xa7\xa6\x06\x13\x68\x75\x5c\x3a\xf9\x1f\x3a\xc7\xd7\x24\xe3\xc0\x09\xfa\xe0\xbd\xed\xa4\xd3\x65\xf1\x99\xa7\x4b\x9f\xd7\x64\xa5\xc6\xb5\x99\xeb\x8f\x9e\x74\x5e\x6e\x68\x83\x72\xf6\xec\x1d\xe3\x9a\x7a\x26\x02\x43\x18\x31\xc4\xbe\xb0\x64\x22\xc4\x69\xb7\xc2\x06\x55\x6f\xcd\x5b\x4a\xc1\xe2\x27\x0a\x0f\xe3\x4d\x6e\xf8\x35\xe9\xf4\x42\xa1\xc2\xa2\xbb\xb5\x47\xb7\x01\x9e\x9e\xe6\x67\x69\xe5\x03\xe0\x9c\xea\x59\xb9\x25\x33\x2d\x1d\x79\x96\xff\xff\x78\xf6\x4f\x00\x00\x00\xff\xff\x0a\x32\xa4\x93\xc2\x12\x00\x00")
+var _rpProductionGlobalJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x58\x6d\x6f\xdb\x36\x10\xfe\x9e\x5f\x21\x68\x03\x64\x03\x96\x25\xf9\x2d\x76\xbe\x79\x2b\x36\x14\xe8\x1a\xc3\x09\xfa\x61\x41\x31\x50\xe4\xd9\xe1\x42\x91\xda\x91\x72\x96\x16\xf9\xef\x83\x5e\xec\x5a\x2f\x96\xbd\x34\x19\xda\x21\x08\x20\x28\xe4\xe9\xf8\xdc\xdd\x73\x0f\x49\x7f\x3e\xb3\x2c\xcb\xb2\x7f\xd4\xf4\x16\x22\x62\x5f\x58\xf6\xad\x31\xb1\xbe\xf0\xbc\x7c\xa4\x1f\x11\x49\xd6\x10\x81\x34\x7d\xf2\x29\x41\xe8\x53\x15\x15\x73\xda\x1b\xf8\xc1\xd8\xf5\x03\xd7\x0f\x3c\x06\xb1\x50\x0f\xa9\xdd\x35\x44\xb1\x20\x06\xfa\x7f\x6a\x25\x7f\xb0\x7b\xf9\x0a\x54\x49\x03\xd2\x7c\x00\xd4\x5c\xc9\x74\xa1\xa0\xef\xa7\x7f\x5b\x83\x98\x20\x89\xc0\x00\x6a\xfb\xc2\xca\x61\x65\xe3\x84\xe2\x3b\x45\x89\xe1\x4a\x5e\x6e\x00\x91\x33\x28\x19\x64\x46\xe6\x21\x4e\x47\x6d\x6d\x90\xcb\x75\xe1\x72\x37\xcb\x60\x45\x12\x61\x3e\x10\x91\x64\x56\xf6\x6e\xfa\xb1\x57\x5a\x68\x09\x5a\x25\x48\xe1\x2d\x3b\xba\x44\xa3\x0b\x2a\x12\x6d\x00\x17\x04\x41\x9a\x37\x2a\x22\x5c\xbe\x27\xd1\x71\xbc\x8d\xce\x56\xf1\x15\xe0\x86\x53\x58\x20\x97\x94\xc7\x44\x3c\x15\x16\xc6\xcf\x83\x08\x9f\x11\x51\x41\x84\x2b\xa3\x90\xac\x61\x4e\xa9\x4a\xa4\xf9\x97\xd0\xce\xf6\xdc\xda\x58\xd4\x2e\xa5\xcf\xcd\xce\xa6\xe2\x4a\xdf\x25\x35\xff\xd9\x84\xcc\x57\xb6\x17\x08\x11\x4f\x22\xbb\x64\xf1\x58\xe1\xd3\xd6\xf8\x46\x27\x61\x0e\xaa\xf3\x85\xbd\x1d\xa7\x44\x24\xa7\xdb\xb3\x08\x63\x1d\x41\xb4\x79\x2b\x19\xfc\x7d\xb9\x6a\x37\x76\xbc\xf4\x19\x74\xbb\x1f\xab\x34\xde\xe6\xe1\x37\x4e\x51\x69\xb5\x32\xfd\x9f\x95\x34\x84\x4b\xc0\x25\xac\xb9\x36\xf8\xe0\x61\xfe\xc2\x41\x57\xbf\x16\x45\x13\x65\xc0\xf9\xaa\x03\x7f\x25\x44\xe8\x2a\x96\x6a\xab\x65\x88\xd2\xc7\x36\xbd\xbf\xa2\x4a\xe2\x4e\xb7\xbf\x75\xd7\xb3\x8e\x7a\xa8\x47\x42\x62\xbe\x27\x04\x03\x7f\xe0\xbb\x41\xaa\x22\x6e\x8c\xb0\xe1\x70\xdf\x48\x9a\x4a\x29\x63\x54\x31\xa0\xe1\x59\xc1\x3f\x1f\x2c\xd1\x3e\xba\x7a\x1f\x38\xa7\x64\xf9\x3d\x98\x7b\x85\x77\x1e\x93\xfa\x77\x25\xdb\x33\xbb\x16\x2a\x24\xe2\x58\xbc\xc1\xd4\xf5\x53\xe1\x7c\xa1\x38\x0f\xc8\xd0\x77\x10\xec\x2e\x1c\xaa\x24\x25\xa6\xf3\xa2\x0d\x96\xbd\xa6\x8f\x2f\xd1\xcf\x13\x73\xab\x90\x7f\xca\x42\x4c\x27\xd7\x09\x67\x9d\x02\x4c\xab\xd7\x32\xcf\xea\x3a\x99\xad\xbc\x5c\x58\x9e\x35\xa7\xb8\x48\x84\x70\xba\x5f\xdd\xe2\x5e\x8c\x6a\xc3\x19\xa0\xf6\x50\x09\x98\x6b\xcd\xd7\x32\xdd\x82\x6b\x55\x2b\x93\xa8\x2e\x7f\x9a\xaa\x7c\xe1\x1b\xdc\x45\xd5\x71\x4e\x43\xe1\xf4\xac\x17\x2d\x53\x2d\x4b\x19\xe2\x34\xe0\x37\xb0\xe2\x92\xa7\xa5\xca\xf6\xa1\x5c\x8f\x29\xf2\x38\x1d\x5a\x36\x06\x52\x2e\x70\xd9\x49\x1a\x89\x73\xbe\x9a\x8d\x03\xc6\x88\x3b\x02\x36\x74\x47\x93\xa9\xef\x92\x73\x4a\xdc\xd1\x70\x05\xc1\xf9\x80\x8d\x87\x53\x56\x6f\xa4\x22\xc9\xfb\xbb\x62\x55\x7a\x9a\x28\xd1\xee\xe6\xba\x20\x43\xf5\xcb\xf6\xcd\xa9\xa9\xfd\x66\xfb\xda\x5a\x3b\x1c\xc5\x20\x99\xbe\x94\xa5\x7d\x73\x37\xfd\x6d\x12\xa2\x84\xf3\xe3\xff\x5a\x58\x9c\x5f\x32\xd1\x58\x5e\x5a\x9b\x91\x55\x4b\xbb\x75\xad\xee\x40\x66\xe3\xc8\xc3\xc4\x28\x7c\xd5\x95\x6f\x52\x57\x46\xd3\xd9\x74\x38\x1e\x8e\xdc\x21\xf3\x27\xee\x88\xd1\xd0\x25\xe3\xc9\xc4\xf5\xa7\x64\x32\x1b\x41\x18\x0c\xce\x67\x4f\xd0\x95\xa6\x4b\xc2\xab\xae\xfc\x77\xba\x72\xf4\x42\x71\x65\x88\x64\x04\xd9\x1f\xef\x96\x57\xed\x09\x3e\xd6\x4b\x44\x08\x75\xff\x93\x50\xe1\x22\x09\x05\xa7\x73\x4a\x41\xa7\xa6\x06\x13\x68\x75\x5c\x3a\xf9\x1f\x3a\xc7\xd7\x24\xe3\xc0\x09\xfa\xe0\xbd\xed\xa4\xd3\x65\xf1\x99\xa7\x4b\x9f\xd7\x64\xa5\xc6\xb5\x99\xeb\x8f\x9e\x74\x5e\x6e\x68\x83\x72\xf6\xec\x1d\xe3\x9a\x7a\x26\x02\x43\x18\x31\xc4\xbe\xb0\x64\x22\xc4\x69\xb7\xc2\x06\x55\x6f\xcd\x5b\x4a\xc1\xe2\x27\x0a\x0f\xe3\x4d\x6e\xf8\x35\xe9\xf4\x42\xa1\xc2\xa2\xbb\xb5\x47\xb7\x01\x9e\x9e\xe6\x67\x69\xe5\x03\xe0\x9c\xea\x59\xb9\x25\x33\x4f\xe9\xc8\xef\xbb\xfe\x8a\x6e\x09\xa0\x5f\x19\xd0\xce\x80\xb3\xfc\xff\xc7\xb3\x7f\x02\x00\x00\xff\xff\x3d\x1a\x2c\x71\xc4\x14\x00\x00")
 
 func rpProductionGlobalJsonBytes() ([]byte, error) {
 	return bindataRead(
diff --git a/pkg/deploy/generator/resources_rp.go b/pkg/deploy/generator/resources_rp.go
index 53d493f83..bc93410ec 100644
--- a/pkg/deploy/generator/resources_rp.go
+++ b/pkg/deploy/generator/resources_rp.go
@@ -1693,6 +1693,19 @@ func (g *generator) rpVersionStorageAccount() []*arm.Resource {
 				"[resourceId('Microsoft.Storage/storageAccounts', parameters('rpVersionStorageAccountName'))]",
 			},
 		},
+		{
+			Resource: &mgmtstorage.BlobContainer{
+				Name: to.StringPtr("[concat(parameters('rpVersionStorageAccountName'), '/default/ocpversions')]"),
+				Type: to.StringPtr("Microsoft.Storage/storageAccounts/blobServices/containers"),
+				ContainerProperties: &mgmtstorage.ContainerProperties{
+					PublicAccess: mgmtstorage.PublicAccessContainer,
+				},
+			},
+			APIVersion: azureclient.APIVersion("Microsoft.Storage"),
+			DependsOn: []string{
+				"[resourceId('Microsoft.Storage/storageAccounts', parameters('rpVersionStorageAccountName'))]",
+			},
+		},
 	}
 }
 
diff --git a/pkg/deploy/upgrade.go b/pkg/deploy/upgrade.go
index 169faa7ad..b2c4ebb28 100644
--- a/pkg/deploy/upgrade.go
+++ b/pkg/deploy/upgrade.go
@@ -6,17 +6,20 @@ package deploy
 import (
 	"bytes"
 	"context"
+	"encoding/json"
 	"net/url"
 	"time"
 
 	mgmtstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-04-01/storage"
 	azstorage "github.com/Azure/azure-sdk-for-go/storage"
 	"github.com/Azure/go-autorest/autorest/date"
+
+	"github.com/Azure/ARO-RP/pkg/util/version"
 )
 
 // SaveVersion for current location in shared storage account for environment
 func (d *deployer) SaveVersion(ctx context.Context) error {
-	d.log.Printf("saving rpVersion %s deployed in %s to storage account %s", d.version, d.config.Location, *d.config.Configuration.RPVersionStorageAccountName)
+	d.log.Printf("saving RP and OCP versions for RP %s deployed in %s to storage account %s", d.version, d.config.Location, *d.config.Configuration.RPVersionStorageAccountName)
 	t := time.Now().UTC().Truncate(time.Second)
 	res, err := d.globalaccounts.ListAccountSAS(
 		ctx, *d.config.Configuration.GlobalResourceGroupName, *d.config.Configuration.RPVersionStorageAccountName, mgmtstorage.AccountSasParameters{
@@ -39,9 +42,20 @@ func (d *deployer) SaveVersion(ctx context.Context) error {
 	blobClient := azstorage.NewAccountSASClient(
 		*d.config.Configuration.RPVersionStorageAccountName, v, (*d.env.Environment()).Environment).GetBlobService()
 
+	// save version of RP which is deployed in this location
 	containerRef := blobClient.GetContainerReference("rpversion")
-
-	// save rpVersion deployed to current location
 	blobRef := containerRef.GetBlobReference(d.config.Location)
-	return blobRef.CreateBlockBlobFromReader(bytes.NewReader([]byte(d.version)), nil)
+	err = blobRef.CreateBlockBlobFromReader(bytes.NewReader([]byte(d.version)), nil)
+	if err != nil {
+		return err
+	}
+
+	// save OCP upgrade streams which are used by RP in this location
+	containerRef = blobClient.GetContainerReference("ocpversions")
+	blobRef = containerRef.GetBlobReference(d.config.Location)
+	streams, err := json.Marshal(version.UpgradeStreams)
+	if err != nil {
+		return err
+	}
+	return blobRef.CreateBlockBlobFromReader(bytes.NewReader(streams), nil)
 }
diff --git a/pkg/frontend/adminactions/upgrade.go b/pkg/frontend/adminactions/upgrade.go
index b7f6f7a96..7199d6fd0 100644
--- a/pkg/frontend/adminactions/upgrade.go
+++ b/pkg/frontend/adminactions/upgrade.go
@@ -19,7 +19,7 @@ import (
 )
 
 func (k *kubeActions) Upgrade(ctx context.Context, upgradeY bool) error {
-	return upgrade(ctx, k.log, k.configcli, version.Streams, upgradeY)
+	return upgrade(ctx, k.log, k.configcli, version.UpgradeStreams, upgradeY)
 }
 
 func upgrade(ctx context.Context, log *logrus.Entry, configcli configclient.Interface, streams []*version.Stream, upgradeY bool) error {
diff --git a/pkg/monitor/cluster/clusterversions.go b/pkg/monitor/cluster/clusterversions.go
index 69ffdcfc1..cf85a4961 100644
--- a/pkg/monitor/cluster/clusterversions.go
+++ b/pkg/monitor/cluster/clusterversions.go
@@ -41,11 +41,11 @@ func (mon *Monitor) emitClusterVersions(ctx context.Context) error {
 	mon.emitGauge("cluster.versions", 1, map[string]string{
 		"actualVersion":                        actualVersion(cv),
 		"desiredVersion":                       desiredVersion(cv),
-		"provisionedByResourceProviderVersion": mon.oc.Properties.ProvisionedBy,       // last successful Put or Patch
-		"resourceProviderVersion":              version.GitCommit,                     // RP version currently running
-		"operatorVersion":                      operatorVersion,                       // operator version in the cluster
-		"availableVersion":                     availableVersion(cv, version.Streams), // current available version for upgrade from stream
-		"availableRP":                          availableRP,                           // current RP version available for document update, empty when none
+		"provisionedByResourceProviderVersion": mon.oc.Properties.ProvisionedBy,              // last successful Put or Patch
+		"resourceProviderVersion":              version.GitCommit,                            // RP version currently running
+		"operatorVersion":                      operatorVersion,                              // operator version in the cluster
+		"availableVersion":                     availableVersion(cv, version.UpgradeStreams), // current available version for upgrade from stream
+		"availableRP":                          availableRP,                                  // current RP version available for document update, empty when none
 	})
 
 	return nil
diff --git a/pkg/util/version/const.go b/pkg/util/version/const.go
index 3858a90e3..c792e53a4 100644
--- a/pkg/util/version/const.go
+++ b/pkg/util/version/const.go
@@ -31,9 +31,9 @@ var InstallStream = &Stream{
 	PullSpec: "quay.io/openshift-release-dev/ocp-release@sha256:60454c3270a61432447c752a9d5ee3e7d84cebcfd5d371eb4daa263623923dac",
 }
 
-// Streams describes list of streams we support for upgrades
+// UpgradeStreams describes list of streams we support for upgrades
 var (
-	Streams = []*Stream{
+	UpgradeStreams = []*Stream{
 		InstallStream,
 		{
 			Version:  NewVersion(4, 6, 40),
diff --git a/pkg/util/version/stream.go b/pkg/util/version/stream.go
index 33840ef17..cf953003b 100644
--- a/pkg/util/version/stream.go
+++ b/pkg/util/version/stream.go
@@ -4,8 +4,8 @@ package version
 // Licensed under the Apache License 2.0.
 
 type Stream struct {
-	Version  *Version
-	PullSpec string
+	Version  *Version `json:"version"`
+	PullSpec string   `json:"-"`
 }
 
 // GetUpgradeStream returns an upgrade Stream for a Version or nil if no upgrade
diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go
index 3496c2972..cccc8dd68 100644
--- a/pkg/util/version/version.go
+++ b/pkg/util/version/version.go
@@ -4,6 +4,7 @@ package version
 // Licensed under the Apache License 2.0.
 
 import (
+	"encoding/json"
 	"fmt"
 	"regexp"
 	"strconv"
@@ -21,6 +22,10 @@ func (v *Version) String() string {
 	return fmt.Sprintf("%d.%d.%d%s", v.V[0], v.V[1], v.V[2], v.Suffix)
 }
 
+func (v *Version) MarshalJSON() ([]byte, error) {
+	return json.Marshal(v.String())
+}
+
 func NewVersion(vs ...uint32) *Version {
 	v := &Version{}
 


Test patch:
diff --git a/pkg/util/version/stream_test.go b/pkg/util/version/stream_test.go
index 853905fef..36a66ad55 100644
--- a/pkg/util/version/stream_test.go
+++ b/pkg/util/version/stream_test.go
@@ -13,7 +13,7 @@ import (
 )
 
 func TestOpenShiftReleaseImages(t *testing.T) {
-	for _, u := range Streams {
+	for _, u := range UpgradeStreams {
 		if !regexp.MustCompile(`^quay.io/openshift-release-dev/ocp-release@sha256:[a-z0-9]{64}$`).MatchString(u.PullSpec) {
 			t.Errorf("PullSpec format invalid: %s", u.PullSpec)
 		}
@@ -21,7 +21,7 @@ func TestOpenShiftReleaseImages(t *testing.T) {
 }
 
 func TestOpenShiftVersions(t *testing.T) {
-	for _, u := range Streams {
+	for _, u := range UpgradeStreams {
 		_, err := ParseVersion(u.Version.String())
 		if err != nil {
 			t.Error(err)
@@ -30,8 +30,8 @@ func TestOpenShiftVersions(t *testing.T) {
 }
 
 func TestUnique(t *testing.T) {
-	unique := make(map[string]int, len(Streams))
-	for _, u := range Streams {
+	unique := make(map[string]int, len(UpgradeStreams))
+	for _, u := range UpgradeStreams {
 		unique[fmt.Sprintf("%d.%d", u.Version.V[0], u.Version.V[1])]++
 	}