diff --git a/go.mod b/go.mod index 9f6766e6c33..de62b7419e9 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/clarketm/json v1.17.1 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/ignition/v2 v2.25.1 + github.com/coreos/stream-metadata-go v0.4.11 github.com/distribution/reference v0.6.0 github.com/docker/distribution v2.8.3+incompatible github.com/elazarl/goproxy v1.7.2 diff --git a/go.sum b/go.sum index 4825c1328e5..98cc20c1167 100644 --- a/go.sum +++ b/go.sum @@ -225,6 +225,8 @@ github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5z github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/coreos/ignition/v2 v2.25.1 h1:mrXOVwb4ZPlLG1tIki5em604Yiz5oRFlWkISrEYpHds= github.com/coreos/ignition/v2 v2.25.1/go.mod h1:Px9MZK4oLhMUM3QMzzRhKbHowc5Hkf+VUu67i0gmsNw= +github.com/coreos/stream-metadata-go v0.4.11 h1:sQQOpI+v/eTyYO76r7jJeCmqZcm1oQYKdwirFJ7f0M0= +github.com/coreos/stream-metadata-go v0.4.11/go.mod h1:dTE8UEFgyUcrbdUg7vGT3uIP7S8a1IwUlmWLKlOp8G8= github.com/coreos/vcontext v0.0.0-20231102161604-685dc7299dc5 h1:sMZSC2BW5LKCdvNbfN12SbKrNvtLBUNjfHZmMvI2ItY= github.com/coreos/vcontext v0.0.0-20231102161604-685dc7299dc5/go.mod h1:Salmysdw7DAVuobBW/LwsKKgpyCPHUhjyJoMJD+ZJiI= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/hypershift-operator/controllers/nodepool/aws.go b/hypershift-operator/controllers/nodepool/aws.go index 021d18cb841..df5223ff2a9 100644 --- a/hypershift-operator/controllers/nodepool/aws.go +++ b/hypershift-operator/controllers/nodepool/aws.go @@ -437,12 +437,14 @@ func getWindowsAMI(region string, specifiedArch string, releaseImage *releaseinf } // Access the rhel-coreos-extensions aws-winli data - winliData := archData.RHCOS.AWSWinLi - if winliData.Regions == nil { + if archData.RHELCoreOSExtensions == nil { + return "", fmt.Errorf("no rhel-coreos-extensions data found in release image metadata") + } + if archData.RHELCoreOSExtensions.AwsWinLi == nil || archData.RHELCoreOSExtensions.AwsWinLi.Regions == nil { return "", fmt.Errorf("no aws-winli regions data found in release image metadata") } - regionData, exists := winliData.Regions[region] + regionData, exists := archData.RHELCoreOSExtensions.AwsWinLi.Regions[region] if !exists { return "", fmt.Errorf("no Windows AMI found for region %s in release image metadata", region) } diff --git a/hypershift-operator/controllers/nodepool/aws_test.go b/hypershift-operator/controllers/nodepool/aws_test.go index 12c1b4dc7a5..9bec3976807 100644 --- a/hypershift-operator/controllers/nodepool/aws_test.go +++ b/hypershift-operator/controllers/nodepool/aws_test.go @@ -27,6 +27,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/coreos/stream-metadata-go/stream" + "github.com/coreos/stream-metadata-go/stream/rhcos" "github.com/google/go-cmp/cmp" ) @@ -269,12 +271,12 @@ func TestAWSMachineTemplateSpec(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ - Regions: map[string]releaseinfo.CoreAWSWinLiRegion{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ + Regions: map[string]rhcos.SingleImage{ "us-east-1": { Release: "418.94.202410090804-0", Image: "ami-0abcdef1234567890", @@ -707,12 +709,50 @@ func TestGetWindowsAMI(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{}, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{}, }, }, expectedError: "couldn't find OS metadata for architecture \"amd64\"", }, + { + name: "When RHELCoreOSExtensions is nil, it should return error", + region: "us-east-1", + arch: hyperv1.ArchitectureAMD64, + releaseImage: &releaseinfo.ReleaseImage{ + ImageStream: &v1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "4.17.0", + }, + }, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "x86_64": {}, + }, + }, + }, + expectedError: "no rhel-coreos-extensions data found in release image metadata", + }, + { + name: "When AwsWinLi is nil, it should return error", + region: "us-east-1", + arch: hyperv1.ArchitectureAMD64, + releaseImage: &releaseinfo.ReleaseImage{ + ImageStream: &v1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "4.17.0", + }, + }, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "x86_64": { + RHELCoreOSExtensions: &rhcos.Extensions{}, + }, + }, + }, + }, + expectedError: "no aws-winli regions data found in release image metadata", + }, { name: "no aws-winli regions data", region: "us-east-1", @@ -723,11 +763,11 @@ func TestGetWindowsAMI(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ Regions: nil, }, }, @@ -747,12 +787,12 @@ func TestGetWindowsAMI(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ - Regions: map[string]releaseinfo.CoreAWSWinLiRegion{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ + Regions: map[string]rhcos.SingleImage{ "us-east-1": { Release: "418.94.202410090804-0", Image: "ami-testimage", @@ -776,12 +816,12 @@ func TestGetWindowsAMI(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ - Regions: map[string]releaseinfo.CoreAWSWinLiRegion{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ + Regions: map[string]rhcos.SingleImage{ "us-east-1": { Release: "418.94.202410090804-0", Image: "", @@ -805,12 +845,12 @@ func TestGetWindowsAMI(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ - Regions: map[string]releaseinfo.CoreAWSWinLiRegion{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ + Regions: map[string]rhcos.SingleImage{ "us-east-1": { Release: "418.94.202410090804-0", Image: "ami-0abcdef1234567890", @@ -838,12 +878,12 @@ func TestGetWindowsAMI(t *testing.T) { Name: "4.17.0", }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ - Regions: map[string]releaseinfo.CoreAWSWinLiRegion{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ + Regions: map[string]rhcos.SingleImage{ "us-east-1": { Release: "418.94.202410090804-0", Image: "ami-0abcdef1234567890", @@ -1048,12 +1088,12 @@ func TestResolveAWSAMI(t *testing.T) { ImageStream: &v1.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: "4.17.0"}, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - RHCOS: releaseinfo.CoreRHCOSImage{ - AWSWinLi: releaseinfo.CoreAWSWinLi{ - Regions: map[string]releaseinfo.CoreAWSWinLiRegion{ + RHELCoreOSExtensions: &rhcos.Extensions{ + AwsWinLi: &rhcos.ReplicatedImage{ + Regions: map[string]rhcos.SingleImage{ "us-east-1": { Release: "418.94.202410090804-0", Image: "ami-windows-us-east-1", diff --git a/hypershift-operator/controllers/nodepool/azure.go b/hypershift-operator/controllers/nodepool/azure.go index 404b993d7eb..43a9252ce90 100644 --- a/hypershift-operator/controllers/nodepool/azure.go +++ b/hypershift-operator/controllers/nodepool/azure.go @@ -133,33 +133,39 @@ func getAzureMarketplaceMetadata(releaseImage *releaseinfo.ReleaseImage, arch st // Extract marketplace metadata from the RHCOS extensions // Structure: .architectures..rhel-coreos-extensions.marketplace.azure.no-purchase-plan // Check for nil safety before accessing nested fields - if archData.RHCOS.Marketplace.Azure.NoPurchasePlan.HyperVGen1 == nil && - archData.RHCOS.Marketplace.Azure.NoPurchasePlan.HyperVGen2 == nil { + if archData.RHELCoreOSExtensions == nil || + archData.RHELCoreOSExtensions.Marketplace == nil || + archData.RHELCoreOSExtensions.Marketplace.Azure == nil || + archData.RHELCoreOSExtensions.Marketplace.Azure.NoPurchasePlan == nil { + return nil, nil // No marketplace data available + } + + azureMarketplace := archData.RHELCoreOSExtensions.Marketplace.Azure.NoPurchasePlan + if azureMarketplace.Gen1 == nil && azureMarketplace.Gen2 == nil { return nil, nil // No marketplace data available } - azureMarketplace := archData.RHCOS.Marketplace.Azure.NoPurchasePlan // Convert from release info format to our internal format result := &azureMarketplaceMetadata{ NoPurchasePlan: &azureMarketplaceImageInfo{}, } - if azureMarketplace.HyperVGen1 != nil { + if azureMarketplace.Gen1 != nil { result.NoPurchasePlan.HyperVGen1 = &hyperv1.AzureMarketplaceImage{ - Publisher: azureMarketplace.HyperVGen1.Publisher, - Offer: azureMarketplace.HyperVGen1.Offer, - SKU: azureMarketplace.HyperVGen1.SKU, - Version: azureMarketplace.HyperVGen1.Version, + Publisher: azureMarketplace.Gen1.Publisher, + Offer: azureMarketplace.Gen1.Offer, + SKU: azureMarketplace.Gen1.SKU, + Version: azureMarketplace.Gen1.Version, ImageGeneration: ptr.To(hyperv1.Gen1), } } - if azureMarketplace.HyperVGen2 != nil { + if azureMarketplace.Gen2 != nil { result.NoPurchasePlan.HyperVGen2 = &hyperv1.AzureMarketplaceImage{ - Publisher: azureMarketplace.HyperVGen2.Publisher, - Offer: azureMarketplace.HyperVGen2.Offer, - SKU: azureMarketplace.HyperVGen2.SKU, - Version: azureMarketplace.HyperVGen2.Version, + Publisher: azureMarketplace.Gen2.Publisher, + Offer: azureMarketplace.Gen2.Offer, + SKU: azureMarketplace.Gen2.SKU, + Version: azureMarketplace.Gen2.Version, ImageGeneration: ptr.To(hyperv1.Gen2), } } diff --git a/hypershift-operator/controllers/nodepool/azure_test.go b/hypershift-operator/controllers/nodepool/azure_test.go index f2580c24f28..471ac81a498 100644 --- a/hypershift-operator/controllers/nodepool/azure_test.go +++ b/hypershift-operator/controllers/nodepool/azure_test.go @@ -16,6 +16,9 @@ import ( capiazure "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" + + "github.com/coreos/stream-metadata-go/stream" + "github.com/coreos/stream-metadata-go/stream/rhcos" ) func TestAzureMachineTemplateSpec(t *testing.T) { @@ -946,6 +949,34 @@ func TestDefaultAzureNodePoolImage(t *testing.T) { expectedImageType: "", expectedMarketplaceImage: nil, }, + { + name: "skip defaulting when RHELCoreOSExtensions is nil", + nodePool: &hyperv1.NodePool{ + Spec: hyperv1.NodePoolSpec{ + Arch: hyperv1.ArchitectureAMD64, + Platform: hyperv1.NodePoolPlatform{ + Type: hyperv1.AzurePlatform, + Azure: &hyperv1.AzureNodePoolPlatform{ + Image: hyperv1.AzureVMImage{}, + }, + }, + }, + }, + releaseImage: &releaseinfo.ReleaseImage{ + ImageStream: &imageapi.ImageStream{ + ObjectMeta: metav1.ObjectMeta{Name: "4.20.0"}, + }, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "x86_64": { + Images: stream.Images{}, + }, + }, + }, + }, + expectedImageType: "", + expectedMarketplaceImage: nil, + }, { name: "skip defaulting when no marketplace metadata available", nodePool: &hyperv1.NodePool{ @@ -1113,11 +1144,11 @@ func TestDefaultAzureNodePoolImage(t *testing.T) { // createMockReleaseImage creates a mock release image for testing func createMockReleaseImage(version string, hasMarketplaceMetadata bool) *releaseinfo.ReleaseImage { - architecture := releaseinfo.CoreOSArchitecture{ - Artifacts: map[string]releaseinfo.CoreOSArtifact{}, - Images: releaseinfo.CoreOSImages{}, - RHCOS: releaseinfo.CoreRHCOSImage{ - AzureDisk: releaseinfo.CoreAzureDisk{ + architecture := stream.Arch{ + Artifacts: map[string]stream.PlatformArtifacts{}, + Images: stream.Images{}, + RHELCoreOSExtensions: &rhcos.Extensions{ + AzureDisk: &rhcos.AzureDisk{ Release: "9.6.20250701-0", URL: "https://rhcos.blob.core.windows.net/imagebucket/rhcos-9.6.20250701-0-azure.x86_64.vhd", }, @@ -1125,16 +1156,16 @@ func createMockReleaseImage(version string, hasMarketplaceMetadata bool) *releas } if hasMarketplaceMetadata { - architecture.RHCOS.Marketplace = releaseinfo.CoreMarketplace{ - Azure: releaseinfo.CoreAzureMarketplace{ - NoPurchasePlan: releaseinfo.CoreAzureMarketplaceNoPurchasePlan{ - HyperVGen1: &releaseinfo.CoreAzureMarketplaceImage{ + architecture.RHELCoreOSExtensions.Marketplace = &rhcos.Marketplace{ + Azure: &rhcos.AzureMarketplace{ + NoPurchasePlan: &rhcos.AzureMarketplaceImages{ + Gen1: &rhcos.AzureMarketplaceImage{ Publisher: "azureopenshift", Offer: "aro4", SKU: "aro_419", Version: "419.6.20250523", }, - HyperVGen2: &releaseinfo.CoreAzureMarketplaceImage{ + Gen2: &rhcos.AzureMarketplaceImage{ Publisher: "azureopenshift", Offer: "aro4", SKU: "419-v2", @@ -1145,12 +1176,12 @@ func createMockReleaseImage(version string, hasMarketplaceMetadata bool) *releas } } - architectures := map[string]releaseinfo.CoreOSArchitecture{ + architectures := map[string]stream.Arch{ "x86_64": architecture, "aarch64": architecture, // ARM64 uses the same marketplace metadata } - streamMetadata := &releaseinfo.CoreOSStreamMetadata{ + streamMetadata := &stream.Stream{ Stream: "test-stream", Architectures: architectures, } diff --git a/hypershift-operator/controllers/nodepool/gcp_test.go b/hypershift-operator/controllers/nodepool/gcp_test.go index 7b5464f5d7a..14a3623acf8 100644 --- a/hypershift-operator/controllers/nodepool/gcp_test.go +++ b/hypershift-operator/controllers/nodepool/gcp_test.go @@ -14,6 +14,8 @@ import ( "k8s.io/utils/ptr" capigcp "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" + + "github.com/coreos/stream-metadata-go/stream" ) func TestGcpMachineTemplateSpec(t *testing.T) { @@ -493,11 +495,11 @@ func TestGcpMachineTemplateSpec(t *testing.T) { ImageStream: &imageapi.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: "4.18.0"}, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{ + Images: stream.Images{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-x86-64-418", }, @@ -542,11 +544,11 @@ func TestDefaultNodePoolGCPImage(t *testing.T) { name: "When stream metadata has project and name for amd64, it should construct image path", arch: hyperv1.ArchitectureAMD64, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{ + Images: stream.Images{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-9-6-20251023-0-gcp-x86-64", }, @@ -562,11 +564,11 @@ func TestDefaultNodePoolGCPImage(t *testing.T) { name: "When stream metadata has project and name for arm64, it should construct image path", arch: hyperv1.ArchitectureARM64, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "aarch64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{ + Images: stream.Images{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-9-6-20251023-0-gcp-aarch64", }, @@ -582,11 +584,11 @@ func TestDefaultNodePoolGCPImage(t *testing.T) { name: "When architecture is not found in release metadata, it should return error", arch: "unsupported-arch", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{ + Images: stream.Images{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-9-6-20251023-0-gcp-x86-64", }, @@ -602,11 +604,11 @@ func TestDefaultNodePoolGCPImage(t *testing.T) { name: "When GCP project and name are empty, it should return error", arch: hyperv1.ArchitectureAMD64, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{}, + Images: stream.Images{ + Gcp: &stream.GcpImage{}, }, }, }, @@ -619,11 +621,11 @@ func TestDefaultNodePoolGCPImage(t *testing.T) { name: "When GCP project is empty but name is set, it should return error", arch: hyperv1.ArchitectureAMD64, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{ + Images: stream.Images{ + Gcp: &stream.GcpImage{ Name: "rhcos-x86-64", }, }, @@ -638,11 +640,11 @@ func TestDefaultNodePoolGCPImage(t *testing.T) { name: "When GCP name is empty but project is set, it should return error", arch: hyperv1.ArchitectureAMD64, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - GCP: releaseinfo.CoreOSGCPImage{ + Images: stream.Images{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", }, }, diff --git a/hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go b/hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go index 7221fedf1be..c5c087a6f3d 100644 --- a/hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go +++ b/hypershift-operator/controllers/nodepool/kubevirt/kubevirt.go @@ -50,7 +50,10 @@ func defaultImage(nodePoolArch string, releaseImage *releaseinfo.ReleaseImage) ( return "", "", fmt.Errorf("couldn't find OS metadata for architecture %q", archName) } - containerImage := arch.Images.Kubevirt.DigestRef + if arch.Images.KubeVirt == nil { + return "", "", fmt.Errorf("no kubevirt image metadata present in release") + } + containerImage := arch.Images.KubeVirt.DigestRef if containerImage == "" { return "", "", fmt.Errorf("no kubevirt vm disk image present in release") } diff --git a/hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go b/hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go index 67f230d9d4e..2084a6b65cc 100644 --- a/hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go +++ b/hypershift-operator/controllers/nodepool/kubevirt/kubevirt_test.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/coreos/stream-metadata-go/stream" "github.com/go-logr/logr" "github.com/go-logr/zapr" "github.com/google/go-cmp/cmp" @@ -1511,18 +1512,18 @@ func generateNodeTemplate(options ...nodeTemplateOption) *capikubevirt.VirtualMa func TestDefaultImage(t *testing.T) { // Create a mock ReleaseImage with architecture-specific Kubevirt images ri := &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ hyperv1.ArchitectureS390X: { - Images: releaseinfo.CoreOSImages{ - Kubevirt: releaseinfo.CoreOSKubevirtImages{ + Images: stream.Images{ + KubeVirt: &stream.ContainerImage{ DigestRef: "quay.io/openshift/release@sha256:s390x1234", }, }, }, hyperv1.ArchAliases[hyperv1.ArchitectureAMD64]: { - Images: releaseinfo.CoreOSImages{ - Kubevirt: releaseinfo.CoreOSKubevirtImages{ + Images: stream.Images{ + KubeVirt: &stream.ContainerImage{ DigestRef: "quay.io/openshift/release@sha256:x86_641234", }, }, @@ -1534,9 +1535,25 @@ func TestDefaultImage(t *testing.T) { tests := []struct { name string arch string + releaseImage *releaseinfo.ReleaseImage expectedImage string expectedDigest string + expectedError bool }{ + { + name: "When KubeVirt image metadata is nil, it should return error", + arch: hyperv1.ArchitectureAMD64, + releaseImage: &releaseinfo.ReleaseImage{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + hyperv1.ArchAliases[hyperv1.ArchitectureAMD64]: { + Images: stream.Images{}, + }, + }, + }, + }, + expectedError: true, + }, { name: "s390x architecture", arch: hyperv1.ArchitectureS390X, @@ -1559,7 +1576,17 @@ func TestDefaultImage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - img, digest, err := defaultImage(tt.arch, ri) + testRI := tt.releaseImage + if testRI == nil { + testRI = ri + } + img, digest, err := defaultImage(tt.arch, testRI) + if tt.expectedError { + if err == nil { + t.Fatalf("expected error but got nil") + } + return + } if err != nil { t.Fatalf("unexpected error for %s: %v", tt.arch, err) } diff --git a/hypershift-operator/controllers/nodepool/nodepool_controller.go b/hypershift-operator/controllers/nodepool/nodepool_controller.go index 8a550972e4a..9d39b3f88ce 100644 --- a/hypershift-operator/controllers/nodepool/nodepool_controller.go +++ b/hypershift-operator/controllers/nodepool/nodepool_controller.go @@ -745,7 +745,10 @@ func defaultNodePoolAMI(region string, specifiedArch string, releaseImage *relea return "", fmt.Errorf("couldn't find OS metadata for architecture %q", specifiedArch) } - regionData, hasRegionData := arch.Images.AWS.Regions[region] + if arch.Images.Aws == nil { + return "", fmt.Errorf("release image metadata has no AWS images") + } + regionData, hasRegionData := arch.Images.Aws.Regions[region] if !hasRegionData { return "", fmt.Errorf("couldn't find AWS image for region %q", region) } @@ -769,10 +772,10 @@ func defaultNodePoolGCPImage(specifiedArch string, releaseImage *releaseinfo.Rel return "", fmt.Errorf("couldn't find OS metadata for architecture %q", specifiedArch) } - if len(arch.Images.GCP.Project) == 0 || len(arch.Images.GCP.Name) == 0 { + if arch.Images.Gcp == nil || len(arch.Images.Gcp.Project) == 0 || len(arch.Images.Gcp.Name) == 0 { return "", fmt.Errorf("release image metadata has no GCP image for architecture %q", specifiedArch) } - return fmt.Sprintf("projects/%s/global/images/%s", arch.Images.GCP.Project, arch.Images.GCP.Name), nil + return fmt.Sprintf("projects/%s/global/images/%s", arch.Images.Gcp.Project, arch.Images.Gcp.Name), nil } // MachineDeploymentComplete considers a MachineDeployment to be complete once all of its desired replicas diff --git a/hypershift-operator/controllers/nodepool/nodepool_controller_test.go b/hypershift-operator/controllers/nodepool/nodepool_controller_test.go index 210b69afcef..824079939a7 100644 --- a/hypershift-operator/controllers/nodepool/nodepool_controller_test.go +++ b/hypershift-operator/controllers/nodepool/nodepool_controller_test.go @@ -42,6 +42,7 @@ import ( "sigs.k8s.io/yaml" ignitionapi "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/stream-metadata-go/stream" "github.com/google/go-cmp/cmp" "github.com/vincent-petithory/dataurl" ) @@ -590,6 +591,21 @@ func TestDefaultNodePoolAMI(t *testing.T) { specifiedArch: "arm64", expectedImage: "", }, + { + name: "fail because Aws images is nil", + region: "us-east-1", + specifiedArch: "amd64", + releaseImage: &releaseinfo.ReleaseImage{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "x86_64": { + Images: stream.Images{}, + }, + }, + }, + }, + expectedImage: "", + }, { name: "fail because stream metadata is nil", region: "us-east-1", @@ -646,6 +662,9 @@ func TestDefaultNodePoolAMI(t *testing.T) { } else if strings.Contains(tc.name, "stream metadata is nil") { g.Expect(tc.image).To(BeEmpty()) g.Expect(tc.err.Error()).To(Equal("release image stream metadata is nil")) + } else if strings.Contains(tc.name, "Aws images is nil") { + g.Expect(tc.image).To(BeEmpty()) + g.Expect(tc.err.Error()).To(Equal("release image metadata has no AWS images")) } else { g.Expect(tc.image).To(BeEmpty()) g.Expect(tc.err.Error()).To(Equal("release image metadata has no image for region \"" + tc.region + "\"")) diff --git a/hypershift-operator/controllers/nodepool/openstack/openstack.go b/hypershift-operator/controllers/nodepool/openstack/openstack.go index 4a824f3e028..48110bd40d1 100644 --- a/hypershift-operator/controllers/nodepool/openstack/openstack.go +++ b/hypershift-operator/controllers/nodepool/openstack/openstack.go @@ -144,12 +144,11 @@ func OpenstackDefaultImage(releaseImage *releaseinfo.ReleaseImage) (string, stri if !exists { return "", "", fmt.Errorf("couldn't find OS metadata for openstack qcow2.gz") } - disk, exists := artifact["disk"] - if !exists { + if artifact.Disk == nil { return "", "", fmt.Errorf("couldn't find OS metadata for the openstack qcow2.gz disk") } - return disk.Location, disk.SHA256, nil + return artifact.Disk.Location, artifact.Disk.Sha256, nil } // OpenStackReleaseImage returns the release version for the OpenStack image. diff --git a/hypershift-operator/controllers/nodepool/openstack/openstack_test.go b/hypershift-operator/controllers/nodepool/openstack/openstack_test.go index c61f6a9fcf3..17da96ff332 100644 --- a/hypershift-operator/controllers/nodepool/openstack/openstack_test.go +++ b/hypershift-operator/controllers/nodepool/openstack/openstack_test.go @@ -13,6 +13,7 @@ import ( capiopenstackv1beta1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "github.com/coreos/stream-metadata-go/stream" "github.com/google/go-cmp/cmp" orc "github.com/k-orc/openstack-resource-controller/api/v1alpha1" ) @@ -212,16 +213,16 @@ func TestOpenstackDefaultImage(t *testing.T) { { name: "valid metadata", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{ + Artifacts: map[string]stream.PlatformArtifacts{ "openstack": { - Formats: map[string]map[string]releaseinfo.CoreOSFormat{ + Formats: map[string]stream.ImageFormat{ "qcow2.gz": { - "disk": { + Disk: &stream.Artifact{ Location: "https://example.com/image.qcow2.gz", - SHA256: "abcdef1234567890", + Sha256: "abcdef1234567890", }, }, }, @@ -237,15 +238,15 @@ func TestOpenstackDefaultImage(t *testing.T) { }, { name: "missing architecture", - releaseImage: &releaseinfo.ReleaseImage{StreamMetadata: &releaseinfo.CoreOSStreamMetadata{Architectures: map[string]releaseinfo.CoreOSArchitecture{}}}, + releaseImage: &releaseinfo.ReleaseImage{StreamMetadata: &stream.Stream{Architectures: map[string]stream.Arch{}}}, expectedError: true, }, { name: "missing openstack artifact", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ - "x86_64": {Artifacts: map[string]releaseinfo.CoreOSArtifact{}}, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "x86_64": {Artifacts: map[string]stream.PlatformArtifacts{}}, }, }, }, @@ -254,11 +255,11 @@ func TestOpenstackDefaultImage(t *testing.T) { { name: "missing qcow2.gz format", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{ - "openstack": {Formats: map[string]map[string]releaseinfo.CoreOSFormat{}}, + Artifacts: map[string]stream.PlatformArtifacts{ + "openstack": {Formats: map[string]stream.ImageFormat{}}, }, }, }, @@ -269,12 +270,12 @@ func TestOpenstackDefaultImage(t *testing.T) { { name: "missing disk artifact", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{ + Artifacts: map[string]stream.PlatformArtifacts{ "openstack": { - Formats: map[string]map[string]releaseinfo.CoreOSFormat{ + Formats: map[string]stream.ImageFormat{ "qcow2.gz": {}, }, }, @@ -319,10 +320,10 @@ func TestOpenStackReleaseImage(t *testing.T) { { name: "valid metadata", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{ + Artifacts: map[string]stream.PlatformArtifacts{ "openstack": { Release: "4.9.0", }, @@ -336,15 +337,15 @@ func TestOpenStackReleaseImage(t *testing.T) { }, { name: "missing architecture", - releaseImage: &releaseinfo.ReleaseImage{StreamMetadata: &releaseinfo.CoreOSStreamMetadata{Architectures: map[string]releaseinfo.CoreOSArchitecture{}}}, + releaseImage: &releaseinfo.ReleaseImage{StreamMetadata: &stream.Stream{Architectures: map[string]stream.Arch{}}}, expectedError: true, }, { name: "missing openstack artifact", releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ - "x86_64": {Artifacts: map[string]releaseinfo.CoreOSArtifact{}}, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "x86_64": {Artifacts: map[string]stream.PlatformArtifacts{}}, }, }, }, @@ -397,17 +398,17 @@ func TestReconcileOpenStackImageSpec(t *testing.T) { }, }, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{ + Artifacts: map[string]stream.PlatformArtifacts{ "openstack": { Release: "4.9.0", - Formats: map[string]map[string]releaseinfo.CoreOSFormat{ + Formats: map[string]stream.ImageFormat{ "qcow2.gz": { - "disk": { + Disk: &stream.Artifact{ Location: "https://example.com/image.qcow2.gz", - SHA256: "abcdef1234567890", + Sha256: "abcdef1234567890", }, }, }, @@ -454,8 +455,8 @@ func TestReconcileOpenStackImageSpec(t *testing.T) { }, }, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ // Missing x86_64 architecture data will cause the OpenstackDefaultImage to fail }, }, @@ -510,10 +511,10 @@ func TestClusterImageName(t *testing.T) { }, }, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{ + Artifacts: map[string]stream.PlatformArtifacts{ "openstack": { Release: "4.19.0", }, @@ -534,8 +535,8 @@ func TestClusterImageName(t *testing.T) { }, }, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{}, + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{}, }, }, expectedError: true, @@ -549,10 +550,10 @@ func TestClusterImageName(t *testing.T) { }, }, releaseImage: &releaseinfo.ReleaseImage{ - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Artifacts: map[string]releaseinfo.CoreOSArtifact{}, + Artifacts: map[string]stream.PlatformArtifacts{}, }, }, }, diff --git a/hypershift-operator/controllers/nodepool/powervs.go b/hypershift-operator/controllers/nodepool/powervs.go index 111250ea233..7f2bf8ee9fb 100644 --- a/hypershift-operator/controllers/nodepool/powervs.go +++ b/hypershift-operator/controllers/nodepool/powervs.go @@ -14,6 +14,8 @@ import ( capipowervs "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2" capiv1 "sigs.k8s.io/cluster-api/api/core/v1beta1" ctrl "sigs.k8s.io/controller-runtime" + + "github.com/coreos/stream-metadata-go/stream" ) const ( @@ -48,7 +50,7 @@ func getImageRegion(region string) string { func ibmPowerVSMachineTemplateSpec(hcluster *hyperv1.HostedCluster, nodePool *hyperv1.NodePool, releaseImage *releaseinfo.ReleaseImage) (*capipowervs.IBMPowerVSMachineTemplateSpec, error) { // Validate PowerVS platform specific input - var coreOSPowerVSImage *releaseinfo.CoreOSPowerVSImage + var coreOSPowerVSImage *stream.SingleObject coreOSPowerVSImage, _, err := getPowerVSImage(hcluster.Spec.Platform.PowerVS.Region, releaseImage) if err != nil { return nil, fmt.Errorf("couldn't discover a PowerVS Image for release image: %w", err) @@ -108,12 +110,16 @@ func (c *CAPI) ibmPowerVSMachineTemplate(templateNameGenerator func(spec any) (s return template, nil } -func getPowerVSImage(region string, releaseImage *releaseinfo.ReleaseImage) (*releaseinfo.CoreOSPowerVSImage, string, error) { +func getPowerVSImage(region string, releaseImage *releaseinfo.ReleaseImage) (*stream.SingleObject, string, error) { arch, foundArch := releaseImage.StreamMetadata.Architectures["ppc64le"] if !foundArch { return nil, "", fmt.Errorf("couldn't find OS metadata for architecture %q", "ppc64le") } + if arch.Images.PowerVS == nil { + return nil, "", fmt.Errorf("release image metadata has no PowerVS images") + } + COSRegion := getImageRegion(region) regionData, hasRegionData := arch.Images.PowerVS.Regions[COSRegion] @@ -135,7 +141,7 @@ func IBMPowerVSImage(namespace, name string) *capipowervs.IBMPowerVSImage { } } -func reconcileIBMPowerVSImage(ibmPowerVSImage *capipowervs.IBMPowerVSImage, hcluster *hyperv1.HostedCluster, nodePool *hyperv1.NodePool, infraID, region string, img *releaseinfo.CoreOSPowerVSImage) error { +func reconcileIBMPowerVSImage(ibmPowerVSImage *capipowervs.IBMPowerVSImage, hcluster *hyperv1.HostedCluster, nodePool *hyperv1.NodePool, infraID, region string, img *stream.SingleObject) error { if ibmPowerVSImage.Annotations == nil { ibmPowerVSImage.Annotations = make(map[string]string) } @@ -155,7 +161,7 @@ func reconcileIBMPowerVSImage(ibmPowerVSImage *capipowervs.IBMPowerVSImage, hclu func (r *NodePoolReconciler) setPowerVSconditions(ctx context.Context, nodePool *hyperv1.NodePool, hcluster *hyperv1.HostedCluster, controlPlaneNamespace string, releaseImage *releaseinfo.ReleaseImage) error { log := ctrl.LoggerFrom(ctx) - var coreOSPowerVSImage *releaseinfo.CoreOSPowerVSImage + var coreOSPowerVSImage *stream.SingleObject coreOSPowerVSImage, powervsImageRegion, err := getPowerVSImage(hcluster.Spec.Platform.PowerVS.Region, releaseImage) if err != nil { SetStatusCondition(&nodePool.Status.Conditions, hyperv1.NodePoolCondition{ diff --git a/hypershift-operator/controllers/nodepool/powervs_test.go b/hypershift-operator/controllers/nodepool/powervs_test.go new file mode 100644 index 00000000000..4b27a0793be --- /dev/null +++ b/hypershift-operator/controllers/nodepool/powervs_test.go @@ -0,0 +1,54 @@ +package nodepool + +import ( + "testing" + + . "github.com/onsi/gomega" + + "github.com/openshift/hypershift/support/releaseinfo" + + "github.com/coreos/stream-metadata-go/stream" +) + +func TestGetPowerVSImage(t *testing.T) { + testCases := []struct { + name string + region string + releaseImage *releaseinfo.ReleaseImage + expectedError string + }{ + { + name: "When PowerVS images is nil, it should return error", + region: "us-south", + releaseImage: &releaseinfo.ReleaseImage{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ + "ppc64le": { + Images: stream.Images{}, + }, + }, + }, + }, + expectedError: "release image metadata has no PowerVS images", + }, + { + name: "When architecture is not found, it should return error", + region: "us-south", + releaseImage: &releaseinfo.ReleaseImage{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{}, + }, + }, + expectedError: "couldn't find OS metadata for architecture", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + _, _, err := getPowerVSImage(tc.region, tc.releaseImage) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + }) + } +} diff --git a/hypershift-operator/controllers/nodepool/token_test.go b/hypershift-operator/controllers/nodepool/token_test.go index 420e1fd8aee..5266d08331c 100644 --- a/hypershift-operator/controllers/nodepool/token_test.go +++ b/hypershift-operator/controllers/nodepool/token_test.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ignitionapi "github.com/coreos/ignition/v2/config/v3_2/types" + "github.com/coreos/stream-metadata-go/stream" "github.com/go-logr/logr/testr" "github.com/google/uuid" ) @@ -1138,12 +1139,12 @@ func TestSetKarpenterAMILabels(t *testing.T) { ImageStream: &imageapi.ImageStream{ ObjectMeta: metav1.ObjectMeta{Name: "test-release"}, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - AWS: releaseinfo.CoreOSAWSImages{ - Regions: map[string]releaseinfo.CoreOSAWSImage{ + Images: stream.Images{ + Aws: &stream.AwsImage{ + Regions: map[string]stream.SingleImage{ "us-east-1": {Image: "ami-amd64-only"}, }, }, diff --git a/support/releaseinfo/deserialize.go b/support/releaseinfo/deserialize.go index 49e8e238e0b..0b031a98a3e 100644 --- a/support/releaseinfo/deserialize.go +++ b/support/releaseinfo/deserialize.go @@ -9,6 +9,8 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/coreos/stream-metadata-go/stream" ) func DeserializeImageStream(data []byte) (*imageapi.ImageStream, error) { @@ -19,7 +21,7 @@ func DeserializeImageStream(data []byte) (*imageapi.ImageStream, error) { return &imageStream, nil } -func DeserializeImageMetadata(data []byte) (*CoreOSStreamMetadata, error) { +func DeserializeImageMetadata(data []byte) (*stream.Stream, error) { var coreOSMetaCM corev1.ConfigMap if err := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(data), 100).Decode(&coreOSMetaCM); err != nil { return nil, fmt.Errorf("couldn't read image lookup data as serialized ConfigMap: %w\nraw data:\n%s", err, string(data)) @@ -28,7 +30,7 @@ func DeserializeImageMetadata(data []byte) (*CoreOSStreamMetadata, error) { if !hasStreamData { return nil, fmt.Errorf("coreos stream metadata configmap is missing the 'stream' key") } - var coreOSMeta CoreOSStreamMetadata + var coreOSMeta stream.Stream if err := json.Unmarshal([]byte(streamData), &coreOSMeta); err != nil { return nil, fmt.Errorf("couldn't decode stream metadata data: %w\n%s", err, streamData) } diff --git a/support/releaseinfo/deserialize_test.go b/support/releaseinfo/deserialize_test.go index 61a7b7c48be..25f357b716c 100644 --- a/support/releaseinfo/deserialize_test.go +++ b/support/releaseinfo/deserialize_test.go @@ -4,6 +4,8 @@ import ( "testing" "github.com/openshift/hypershift/support/releaseinfo/fixtures" + + "github.com/coreos/stream-metadata-go/stream" ) func TestDeserializeImageStream(t *testing.T) { @@ -16,18 +18,19 @@ func TestDeserializeImageStream(t *testing.T) { func TestDeserializeImageMetadata(t *testing.T) { for _, imageMetadata := range [][]byte{fixtures.CoreOSBootImagesYAML_4_8, fixtures.CoreOSBootImagesYAML_4_10} { - var coreOSMetadata *CoreOSStreamMetadata + var coreOSMetadata *stream.Stream coreOSMetadata, err := DeserializeImageMetadata(imageMetadata) if err != nil { t.Fatal(err) } - if _, ok := coreOSMetadata.Architectures["x86_64"]; !ok { - t.Fatal(err) + arch, ok := coreOSMetadata.Architectures["x86_64"] + if !ok { + t.Fatal("missing x86_64 architecture") } - if coreOSMetadata.Architectures["x86_64"].RHCOS.AzureDisk.URL == "" { - t.Fatal(err) + if arch.RHELCoreOSExtensions == nil || arch.RHELCoreOSExtensions.AzureDisk == nil || arch.RHELCoreOSExtensions.AzureDisk.URL == "" { + t.Fatal("missing azure disk URL") } } diff --git a/support/releaseinfo/fake/fake.go b/support/releaseinfo/fake/fake.go index 8a3bd60100a..bd5866aa04a 100644 --- a/support/releaseinfo/fake/fake.go +++ b/support/releaseinfo/fake/fake.go @@ -16,6 +16,8 @@ import ( "k8s.io/apimachinery/pkg/types" crclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/coreos/stream-metadata-go/stream" ) var _ releaseinfo.ProviderWithRegistryOverrides = &FakeReleaseProvider{} @@ -69,28 +71,28 @@ func (f *FakeReleaseProvider) Lookup(_ context.Context, image string, _ []byte) }, }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - AWS: releaseinfo.CoreOSAWSImages{ - Regions: map[string]releaseinfo.CoreOSAWSImage{ + Images: stream.Images{ + Aws: &stream.AwsImage{ + Regions: map[string]stream.SingleImage{ "us-east-1": { Release: "us-east-1-x86_64-release", Image: "us-east-1-x86_64-image", }, }, }, - GCP: releaseinfo.CoreOSGCPImage{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-x86-64-fake", }, }, }, "aarch64": { - Images: releaseinfo.CoreOSImages{ - AWS: releaseinfo.CoreOSAWSImages{ - Regions: map[string]releaseinfo.CoreOSAWSImage{ + Images: stream.Images{ + Aws: &stream.AwsImage{ + Regions: map[string]stream.SingleImage{ "us-east-1": { Release: "us-east-1-aarch64-release", Image: "us-east-1-aarch64-image", @@ -101,7 +103,7 @@ func (f *FakeReleaseProvider) Lookup(_ context.Context, image string, _ []byte) }, }, }, - GCP: releaseinfo.CoreOSGCPImage{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-aarch64-fake", }, diff --git a/support/releaseinfo/registry_image_content_policies_test.go b/support/releaseinfo/registry_image_content_policies_test.go index 683ff9cf7d9..b8f8b423b11 100644 --- a/support/releaseinfo/registry_image_content_policies_test.go +++ b/support/releaseinfo/registry_image_content_policies_test.go @@ -11,6 +11,7 @@ import ( imagev1 "github.com/openshift/api/image/v1" + "github.com/coreos/stream-metadata-go/stream" "github.com/docker/distribution" ) @@ -22,7 +23,7 @@ func TestProviderWithOpenShiftImageRegistryOverridesDecorator_Lookup(t *testing. canonicalReleaseImage := "canonical-release-image" releaseImage := &ReleaseImage{ ImageStream: &imagev1.ImageStream{}, - StreamMetadata: &CoreOSStreamMetadata{}, + StreamMetadata: &stream.Stream{}, } // Create registry providers delegating to a cached provider so we can mock the cache content for the mirroredReleaseImage. @@ -64,7 +65,7 @@ func TestProviderWithOpenShiftImageRegistryOverridesDecorator_LookupWithNilRepoS directImage := "quay.io/openshift-release-dev/ocp-release:4.16.13-x86_64" releaseImage := &ReleaseImage{ ImageStream: &imagev1.ImageStream{}, - StreamMetadata: &CoreOSStreamMetadata{}, + StreamMetadata: &stream.Stream{}, } delegate := &RegistryMirrorProviderDecorator{ diff --git a/support/releaseinfo/releaseinfo.go b/support/releaseinfo/releaseinfo.go index 5e21316fe4f..280cd876761 100644 --- a/support/releaseinfo/releaseinfo.go +++ b/support/releaseinfo/releaseinfo.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "github.com/blang/semver" + "github.com/coreos/stream-metadata-go/stream" ) // Provider knows how to find the release image metadata for an image referred @@ -38,120 +39,7 @@ type ProviderWithOpenShiftImageRegistryOverrides interface { // discover constituent component image information. type ReleaseImage struct { *imageapi.ImageStream `json:",inline"` - StreamMetadata *CoreOSStreamMetadata `json:"streamMetadata"` -} - -type CoreOSStreamMetadata struct { - Stream string `json:"stream"` - Architectures map[string]CoreOSArchitecture `json:"architectures"` -} - -type CoreOSArchitecture struct { - // Artifacts is a map of platform name to Artifacts - Artifacts map[string]CoreOSArtifact `json:"artifacts"` - Images CoreOSImages `json:"images"` - RHCOS CoreRHCOSImage `json:"rhel-coreos-extensions"` -} - -type CoreOSArtifact struct { - Release string `json:"release"` - Formats map[string]map[string]CoreOSFormat `json:"formats"` -} - -type CoreOSFormat struct { - Location string `json:"location"` - Signature string `json:"signature"` - SHA256 string `json:"sha256"` - UncompressedSHA256 string `json:"uncompressed-sha256"` -} - -type CoreOSImages struct { - AWS CoreOSAWSImages `json:"aws"` - GCP CoreOSGCPImage `json:"gcp"` - PowerVS CoreOSPowerVSImages `json:"powervs"` - Kubevirt CoreOSKubevirtImages `json:"kubevirt"` -} - -// CoreOSGCPImage contains GCP image information from stream metadata. -// GCP images are global (not regional like AWS), so there's a single image reference. -// The image path is constructed as projects/{Project}/global/images/{Name}. -type CoreOSGCPImage struct { - // Project is the GCP project hosting the image (e.g., rhcos-cloud) - Project string `json:"project"` - // Name is the image name within the project - Name string `json:"name"` - // Family is the image family (optional, used for latest image lookups) - Family string `json:"family"` -} - -type CoreRHCOSImage struct { - AzureDisk CoreAzureDisk `json:"azure-disk"` - Marketplace CoreMarketplace `json:"marketplace"` - AWSWinLi CoreAWSWinLi `json:"aws-winli"` -} - -type CoreAzureDisk struct { - Release string `json:"release"` - URL string `json:"url"` -} - -// CoreMarketplace represents marketplace information for different cloud providers -type CoreMarketplace struct { - Azure CoreAzureMarketplace `json:"azure"` -} - -// CoreAzureMarketplace contains Azure marketplace image information -type CoreAzureMarketplace struct { - NoPurchasePlan CoreAzureMarketplaceNoPurchasePlan `json:"no-purchase-plan"` -} - -// CoreAzureMarketplaceNoPurchasePlan contains marketplace images that don't require a purchase plan -type CoreAzureMarketplaceNoPurchasePlan struct { - HyperVGen1 *CoreAzureMarketplaceImage `json:"hyperVGen1,omitempty"` - HyperVGen2 *CoreAzureMarketplaceImage `json:"hyperVGen2,omitempty"` -} - -// CoreAzureMarketplaceImage represents an Azure marketplace image specification -type CoreAzureMarketplaceImage struct { - Publisher string `json:"publisher"` - Offer string `json:"offer"` - SKU string `json:"sku"` - Version string `json:"version"` -} - -type CoreAWSWinLi struct { - Regions map[string]CoreAWSWinLiRegion `json:"regions"` -} - -type CoreAWSWinLiRegion struct { - Release string `json:"release"` - Image string `json:"image"` -} - -type CoreOSAWSImages struct { - Regions map[string]CoreOSAWSImage `json:"regions"` -} - -type CoreOSAWSImage struct { - Release string `json:"release"` - Image string `json:"image"` -} - -type CoreOSKubevirtImages struct { - Release string `json:"release"` - Image string `json:"image"` - DigestRef string `json:"digest-ref"` -} - -type CoreOSPowerVSImages struct { - Regions map[string]CoreOSPowerVSImage `json:"regions"` -} - -type CoreOSPowerVSImage struct { - Release string `json:"release"` - Object string `json:"object"` - Bucket string `json:"bucket"` - URL string `json:"url"` + StreamMetadata *stream.Stream `json:"streamMetadata"` } func (i *ReleaseImage) Version() string { diff --git a/support/releaseinfo/releaseinfo_test.go b/support/releaseinfo/releaseinfo_test.go index 199d944b4bb..cba7521a5c4 100644 --- a/support/releaseinfo/releaseinfo_test.go +++ b/support/releaseinfo/releaseinfo_test.go @@ -172,11 +172,11 @@ func TestReleaseInfoPowerVS(t *testing.T) { if !ok { t.Fatal("metadata does not contain the ppc64le architecture") } - if len(arch.Images.PowerVS.Regions) == 0 { + if arch.Images.PowerVS == nil || len(arch.Images.PowerVS.Regions) == 0 { t.Fatal("metadata does not contain any powervs regions") } for _, region := range arch.Images.PowerVS.Regions { - if region.Release == "" || region.Object == "" || region.Bucket == "" || region.URL == "" { + if region.Release == "" || region.Object == "" || region.Bucket == "" || region.Url == "" { t.Fatalf("none of the fields in the image can be empty: %+v", region) } } @@ -192,7 +192,7 @@ func TestReleaseInfoKubeVirt(t *testing.T) { if !ok { t.Fatal("metadata does not contain the x86_64 architecture") } - if arch.Images.Kubevirt.DigestRef == "" { + if arch.Images.KubeVirt == nil || arch.Images.KubeVirt.DigestRef == "" { t.Fatal("metadata does not contain a digest ref for kubevirt") } } diff --git a/support/releaseinfo/testutils/testutils.go b/support/releaseinfo/testutils/testutils.go index fc42b0ac5cc..e85440eddd1 100644 --- a/support/releaseinfo/testutils/testutils.go +++ b/support/releaseinfo/testutils/testutils.go @@ -9,6 +9,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/coreos/stream-metadata-go/stream" ) // InitReleaseImageOrDie returns a ptr to releaseinfo.ReleaseImage, @@ -58,28 +60,28 @@ func InitReleaseImageOrDie(version string) *releaseinfo.ReleaseImage { }, }, }, - StreamMetadata: &releaseinfo.CoreOSStreamMetadata{ - Architectures: map[string]releaseinfo.CoreOSArchitecture{ + StreamMetadata: &stream.Stream{ + Architectures: map[string]stream.Arch{ "x86_64": { - Images: releaseinfo.CoreOSImages{ - AWS: releaseinfo.CoreOSAWSImages{ - Regions: map[string]releaseinfo.CoreOSAWSImage{ + Images: stream.Images{ + Aws: &stream.AwsImage{ + Regions: map[string]stream.SingleImage{ "us-east-1": { Release: "us-east-1-x86_64-release", Image: "us-east-1-x86_64-image", }, }, }, - GCP: releaseinfo.CoreOSGCPImage{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-x86-64-test", }, }, }, "aarch64": { - Images: releaseinfo.CoreOSImages{ - AWS: releaseinfo.CoreOSAWSImages{ - Regions: map[string]releaseinfo.CoreOSAWSImage{ + Images: stream.Images{ + Aws: &stream.AwsImage{ + Regions: map[string]stream.SingleImage{ "us-east-1": { Release: "us-east-1-aarch64-release", Image: "us-east-1-aarch64-image", @@ -90,7 +92,7 @@ func InitReleaseImageOrDie(version string) *releaseinfo.ReleaseImage { }, }, }, - GCP: releaseinfo.CoreOSGCPImage{ + Gcp: &stream.GcpImage{ Project: "rhcos-cloud", Name: "rhcos-aarch64-test", }, diff --git a/vendor/github.com/coreos/stream-metadata-go/LICENSE b/vendor/github.com/coreos/stream-metadata-go/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go b/vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go new file mode 100644 index 00000000000..312a94b7b9b --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/artifact_utils.go @@ -0,0 +1,98 @@ +package stream + +import ( + "crypto/sha256" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" +) + +// Fetch an artifact, validating its checksum. If applicable, +// the artifact will not be decompressed. Does not +// validate GPG signature. +func (a *Artifact) Fetch(w io.Writer) error { + resp, err := http.Get(a.Location) + if err != nil { + return err + } + defer func() { + err = errors.Join(resp.Body.Close()) + }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%s returned status: %s", a.Location, resp.Status) + } + hasher := sha256.New() + reader := io.TeeReader(resp.Body, hasher) + + _, err = io.Copy(w, reader) + if err != nil { + return err + } + + // Validate sha256 checksum + foundChecksum := fmt.Sprintf("%x", hasher.Sum(nil)) + if a.Sha256 != foundChecksum { + return fmt.Errorf("checksum mismatch for %s; expected=%s found=%s", a.Location, a.Sha256, foundChecksum) + } + + return nil +} + +// Name returns the "basename" of the artifact, i.e. the contents +// after the last `/`. This can be useful when downloading to a file. +func (a *Artifact) Name() (string, error) { + loc, err := url.Parse(a.Location) + if err != nil { + return "", fmt.Errorf("failed to parse artifact url: %w", err) + } + // Note this one uses `path` since even on Windows URLs have forward slashes. + return path.Base(loc.Path), nil +} + +// Download fetches the specified artifact and saves it to the target +// directory. The full file path will be returned as a string. +// If the target file path exists, it will be overwritten. +// If the download fails, the temporary file will be deleted. +func (a *Artifact) Download(destdir string) (string, error) { + name, err := a.Name() + if err != nil { + return "", err + } + destfile := filepath.Join(destdir, name) + w, err := os.CreateTemp(destdir, ".coreos-artifact-") + if err != nil { + return "", err + } + finalized := false + defer func() { + if !finalized { + // Ignore an error to unlink + _ = os.Remove(w.Name()) + } + }() + + if err := a.Fetch(w); err != nil { + return "", err + } + if err := w.Sync(); err != nil { + return "", err + } + if err := w.Chmod(0644); err != nil { + return "", err + } + if err := w.Close(); err != nil { + return "", err + } + if err := os.Rename(w.Name(), destfile); err != nil { + return "", err + } + finalized = true + + return destfile, nil +} diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go b/vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go new file mode 100644 index 00000000000..62616b3b278 --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/rhcos/rhcos.go @@ -0,0 +1,92 @@ +package rhcos + +import "fmt" + +// Extensions is data specific to Red Hat Enterprise Linux CoreOS +type Extensions struct { + AwsWinLi *AwsWinLi `json:"aws-winli,omitempty"` + AzureDisk *AzureDisk `json:"azure-disk,omitempty"` + Marketplace *Marketplace `json:"marketplace,omitempty"` +} + +// AzureDisk represents an Azure disk image that can be imported +// into an image gallery or otherwise replicated, and then used +// as a boot source for virtual machines. +type AzureDisk struct { + // Release is the source release version + Release string `json:"release"` + // URL to an image already stored in Azure infrastructure + // that can be copied into an image gallery. Avoid creating VMs directly + // from this URL as that may lead to performance limitations. + URL string `json:"url,omitempty"` +} + +// AwsWinLi represents prebuilt AWS Windows License Included Images. +type AwsWinLi = ReplicatedImage + +// ReplicatedImage represents an image in all regions of an AWS-like cloud +// This struct was copied from the release package to avoid an import cycle, +// and is used to describe all AWS WinLI Images in all regions. +type ReplicatedImage struct { + Regions map[string]SingleImage `json:"regions,omitempty"` +} + +// SingleImage represents a globally-accessible image or an image in a +// single region of an AWS-like cloud +// This struct was copied from the release package to avoid an import cycle, +// and is used to describe individual AWS WinLI Images. +type SingleImage struct { + Release string `json:"release"` + Image string `json:"image"` +} + +// Marketplace contains marketplace images for all clouds. +type Marketplace struct { + Azure *AzureMarketplace `json:"azure,omitempty"` +} + +// AzureMarketplaceImages contains both the HyperV- Gen1 & Gen2 +// images for a purchase plan. +type AzureMarketplaceImages struct { + Gen1 *AzureMarketplaceImage `json:"hyperVGen1,omitempty"` + Gen2 *AzureMarketplaceImage `json:"hyperVGen2,omitempty"` +} + +// AzureMarketplace lists images, both paid and +// unpaid, available in the Azure marketplace. +type AzureMarketplace struct { + // NoPurchasePlan is the standard, unpaid RHCOS image. + NoPurchasePlan *AzureMarketplaceImages `json:"no-purchase-plan,omitempty"` + + // OCP is the paid marketplace image for OpenShift Container Platform. + OCP *AzureMarketplaceImages `json:"ocp,omitempty"` + + // OPP is the paid marketplace image for OpenShift Platform Plus. + OPP *AzureMarketplaceImages `json:"opp,omitempty"` + + // OKE is the paid marketplace image for OpenShift Kubernetes Engine. + OKE *AzureMarketplaceImages `json:"oke,omitempty"` + + // OCPEMEA is the paid marketplace image for OpenShift Container Platform in EMEA regions. + OCPEMEA *AzureMarketplaceImages `json:"ocp-emea,omitempty"` + + // OPPEMEA is the paid marketplace image for OpenShift Platform Plus in EMEA regions. + OPPEMEA *AzureMarketplaceImages `json:"opp-emea,omitempty"` + + // OKEEMEA is the paid marketplace image for OpenShift Kubernetes Engine in EMEA regions. + OKEEMEA *AzureMarketplaceImages `json:"oke-emea,omitempty"` +} + +// AzureMarketplaceImage defines the attributes for an Azure +// marketplace image. +type AzureMarketplaceImage struct { + Publisher string `json:"publisher"` + Offer string `json:"offer"` + SKU string `json:"sku"` + Version string `json:"version"` +} + +// URN returns the image URN for the marketplace image. +func (i *AzureMarketplaceImage) URN() string { + return fmt.Sprintf("%s:%s:%s:%s", i.Publisher, i.Offer, i.SKU, i.Version) +} diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/stream.go b/vendor/github.com/coreos/stream-metadata-go/stream/stream.go new file mode 100644 index 00000000000..1ed9b1fd70f --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/stream.go @@ -0,0 +1,116 @@ +// Package stream models a CoreOS "stream", which is +// a description of the recommended set of binary images for CoreOS. Use +// this API to find cloud images, bare metal disk images, etc. +package stream + +import ( + "github.com/coreos/stream-metadata-go/stream/rhcos" +) + +// Stream contains artifacts available in a stream +type Stream struct { + Stream string `json:"stream"` + Metadata Metadata `json:"metadata"` + Architectures map[string]Arch `json:"architectures"` +} + +// Metadata for a release or stream +type Metadata struct { + LastModified string `json:"last-modified"` + Generator string `json:"generator,omitempty"` +} + +// Arch contains release details for a particular hardware architecture +type Arch struct { + Artifacts map[string]PlatformArtifacts `json:"artifacts"` + Images Images `json:"images,omitempty"` + // RHELCoreOSExtensions is data specific to Red Hat Enterprise Linux CoreOS + RHELCoreOSExtensions *rhcos.Extensions `json:"rhel-coreos-extensions,omitempty"` +} + +// PlatformArtifacts contains images for a platform +type PlatformArtifacts struct { + Release string `json:"release"` + Formats map[string]ImageFormat `json:"formats"` +} + +// ImageFormat contains all artifacts for a single OS image +type ImageFormat struct { + Disk *Artifact `json:"disk,omitempty"` + Kernel *Artifact `json:"kernel,omitempty"` + Initramfs *Artifact `json:"initramfs,omitempty"` + Rootfs *Artifact `json:"rootfs,omitempty"` +} + +// Artifact represents one image file, plus its metadata +type Artifact struct { + Location string `json:"location"` + Signature string `json:"signature,omitempty"` + Sha256 string `json:"sha256"` + UncompressedSha256 string `json:"uncompressed-sha256,omitempty"` +} + +// Images contains images available in cloud providers +type Images struct { + Aliyun *ReplicatedImage `json:"aliyun,omitempty"` + Aws *AwsImage `json:"aws,omitempty"` + Gcp *GcpImage `json:"gcp,omitempty"` + Ibmcloud *ReplicatedObject `json:"ibmcloud,omitempty"` + KubeVirt *ContainerImage `json:"kubevirt,omitempty"` + PowerVS *ReplicatedObject `json:"powervs,omitempty"` +} + +// ReplicatedImage represents an image in all regions of an AWS-like cloud +type ReplicatedImage struct { + Regions map[string]SingleImage `json:"regions,omitempty"` +} + +// SingleImage represents a globally-accessible image or an image in a +// single region of an AWS-like cloud +type SingleImage struct { + Release string `json:"release"` + Image string `json:"image"` +} + +// ContainerImage represents a tagged container image +type ContainerImage struct { + Release string `json:"release"` + // Preferred way to reference the image, which might be by tag or digest + Image string `json:"image"` + DigestRef string `json:"digest-ref"` +} + +// AwsImage is a typedef for backwards compatibility. +type AwsImage = ReplicatedImage + +// AwsRegionImage is a typedef for backwards compatibility. +type AwsRegionImage = SingleImage + +// RegionImage is a typedef for backwards compatibility. +type RegionImage = SingleImage + +// GcpImage represents a GCP cloud image +type GcpImage struct { + Release string `json:"release"` + Project string `json:"project"` + Family string `json:"family,omitempty"` + Name string `json:"name"` +} + +// ReplicatedObject represents an object in all regions of an IBMCloud-like +// cloud +type ReplicatedObject struct { + Regions map[string]SingleObject `json:"regions,omitempty"` +} + +// SingleObject represents a globally-accessible cloud storage object, or +// an object in a single region of an IBMCloud-like cloud +type SingleObject struct { + Release string `json:"release"` + Object string `json:"object"` + Bucket string `json:"bucket"` + Url string `json:"url"` +} + +// RegionObject is a typedef for backwards compatibility. +type RegionObject = SingleObject diff --git a/vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go b/vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go new file mode 100644 index 00000000000..b1ca955641e --- /dev/null +++ b/vendor/github.com/coreos/stream-metadata-go/stream/stream_utils.go @@ -0,0 +1,94 @@ +package stream + +import "fmt" + +// FormatPrefix describes a stream+architecture combination, intended for prepending to error messages +func (st *Stream) FormatPrefix(archname string) string { + return fmt.Sprintf("%s/%s", st.Stream, archname) +} + +// GetArchitecture loads the architecture-specific builds from a stream, +// with a useful descriptive error message if the architecture is not found. +func (st *Stream) GetArchitecture(archname string) (*Arch, error) { + archdata, ok := st.Architectures[archname] + if !ok { + return nil, fmt.Errorf("stream:%s does not have architecture '%s'", st.Stream, archname) + } + return &archdata, nil +} + +// GetAliyunRegionImage returns the release data (Image ID and release ID) for a particular +// architecture and region. +func (st *Stream) GetAliyunRegionImage(archname, region string) (*SingleImage, error) { + starch, err := st.GetArchitecture(archname) + if err != nil { + return nil, err + } + aliyunimages := starch.Images.Aliyun + if aliyunimages == nil { + return nil, fmt.Errorf("%s: No Aliyun images", st.FormatPrefix(archname)) + } + var regionVal SingleImage + var ok bool + if regionVal, ok = aliyunimages.Regions[region]; !ok { + return nil, fmt.Errorf("%s: No Aliyun images in region %s", st.FormatPrefix(archname), region) + } + + return ®ionVal, nil +} + +// GetAliyunImage returns the Aliyun image for a particular architecture and region. +func (st *Stream) GetAliyunImage(archname, region string) (string, error) { + regionVal, err := st.GetAliyunRegionImage(archname, region) + if err != nil { + return "", err + } + return regionVal.Image, nil +} + +// GetAwsRegionImage returns the release data (AMI and release ID) for a particular +// architecture and region. +func (st *Stream) GetAwsRegionImage(archname, region string) (*SingleImage, error) { + starch, err := st.GetArchitecture(archname) + if err != nil { + return nil, err + } + awsimages := starch.Images.Aws + if awsimages == nil { + return nil, fmt.Errorf("%s: No AWS images", st.FormatPrefix(archname)) + } + var regionVal SingleImage + var ok bool + if regionVal, ok = awsimages.Regions[region]; !ok { + return nil, fmt.Errorf("%s: No AWS images in region %s", st.FormatPrefix(archname), region) + } + + return ®ionVal, nil +} + +// GetAMI returns the AWS machine image for a particular architecture and region. +func (st *Stream) GetAMI(archname, region string) (string, error) { + regionVal, err := st.GetAwsRegionImage(archname, region) + if err != nil { + return "", err + } + return regionVal.Image, nil +} + +// QueryDisk finds the singleton disk artifact for a given format and architecture. +func (st *Stream) QueryDisk(architectureName, artifactName, formatName string) (*Artifact, error) { + arch, err := st.GetArchitecture(architectureName) + if err != nil { + return nil, err + } + artifacts := arch.Artifacts[artifactName] + if artifacts.Release == "" { + return nil, fmt.Errorf("%s: artifact '%s' not found", st.FormatPrefix(architectureName), artifactName) + } + format := artifacts.Formats[formatName] + if format.Disk == nil { + return nil, fmt.Errorf("%s: artifact '%s' format '%s' disk not found", st.FormatPrefix(architectureName), artifactName, formatName) + } + + return format.Disk, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5c8b8c880d9..4b11722d8c1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -549,6 +549,10 @@ github.com/coreos/ignition/v2/config/shared/parse github.com/coreos/ignition/v2/config/shared/validations github.com/coreos/ignition/v2/config/util github.com/coreos/ignition/v2/config/v3_2/types +# github.com/coreos/stream-metadata-go v0.4.11 +## explicit; go 1.18 +github.com/coreos/stream-metadata-go/stream +github.com/coreos/stream-metadata-go/stream/rhcos # github.com/coreos/vcontext v0.0.0-20231102161604-685dc7299dc5 ## explicit; go 1.18 github.com/coreos/vcontext/path