diff --git a/cmd/cluster/aws/create.go b/cmd/cluster/aws/create.go index c9b0592451..6744b35ecd 100644 --- a/cmd/cluster/aws/create.go +++ b/cmd/cluster/aws/create.go @@ -125,20 +125,7 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur } } if infra == nil { - opt := awsinfra.CreateInfraOptions{ - Region: opts.AWSPlatform.Region, - InfraID: opts.InfraID, - AWSCredentialsOpts: opts.AWSPlatform.AWSCredentialsOpts, - Name: opts.Name, - BaseDomain: opts.BaseDomain, - BaseDomainPrefix: opts.BaseDomainPrefix, - AdditionalTags: opts.AWSPlatform.AdditionalTags, - Zones: opts.AWSPlatform.Zones, - EnableProxy: opts.AWSPlatform.EnableProxy, - SSHKeyFile: opts.SSHKeyFile, - SingleNATGateway: opts.AWSPlatform.SingleNATGateway, - CredentialsSecretData: secretData, - } + opt := CreateInfraOptions(opts) infra, err = opt.CreateInfra(ctx, opts.Log) if err != nil { return fmt.Errorf("failed to create infra: %w", err) @@ -156,18 +143,7 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur return fmt.Errorf("failed to load infra json: %w", err) } } else { - opt := awsinfra.CreateIAMOptions{ - Region: opts.AWSPlatform.Region, - AWSCredentialsOpts: opts.AWSPlatform.AWSCredentialsOpts, - InfraID: infra.InfraID, - IssuerURL: opts.AWSPlatform.IssuerURL, - AdditionalTags: opts.AWSPlatform.AdditionalTags, - PrivateZoneID: infra.PrivateZoneID, - PublicZoneID: infra.PublicZoneID, - LocalZoneID: infra.LocalZoneID, - KMSKeyARN: opts.AWSPlatform.EtcdKMSKeyARN, - CredentialsSecretData: secretData, - } + opt := CreateIAMOptions(opts, infra) iamInfo, err = opt.CreateIAM(ctx, client) if err != nil { return fmt.Errorf("failed to create iam: %w", err) @@ -236,6 +212,44 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur return nil } +func CreateInfraOptions(opts *core.CreateOptions) awsinfra.CreateInfraOptions { + return awsinfra.CreateInfraOptions{ + Region: opts.AWSPlatform.Region, + InfraID: opts.InfraID, + AWSCredentialsOpts: opts.AWSPlatform.AWSCredentialsOpts, + Name: opts.Name, + BaseDomain: opts.BaseDomain, + BaseDomainPrefix: opts.BaseDomainPrefix, + AdditionalTags: opts.AWSPlatform.AdditionalTags, + Zones: opts.AWSPlatform.Zones, + EnableProxy: opts.AWSPlatform.EnableProxy, + SSHKeyFile: opts.SSHKeyFile, + SingleNATGateway: opts.AWSPlatform.SingleNATGateway, + } +} + +func CreateIAMOptions(opts *core.CreateOptions, infra *awsinfra.CreateInfraOutput) awsinfra.CreateIAMOptions { + return awsinfra.CreateIAMOptions{ + Region: opts.AWSPlatform.Region, + AWSCredentialsOpts: opts.AWSPlatform.AWSCredentialsOpts, + InfraID: infra.InfraID, + IssuerURL: opts.AWSPlatform.IssuerURL, + AdditionalTags: opts.AWSPlatform.AdditionalTags, + PrivateZoneID: infra.PrivateZoneID, + PublicZoneID: infra.PublicZoneID, + LocalZoneID: infra.LocalZoneID, + KMSKeyARN: opts.AWSPlatform.EtcdKMSKeyARN, + } +} + +// IsRequiredOption returns a cobra style error message when the flag value is empty +func IsRequiredOption(flag string, value string) error { + if len(value) == 0 { + return fmt.Errorf("required flag(s) \"%s\" not set", flag) + } + return nil +} + // ValidateCreateCredentialInfo validates if the credentials secret name is empty that the aws-creds and pull-secret flags are // not empty; validates if the credentials secret is not empty, that it can be retrieved func ValidateCreateCredentialInfo(opts awsutil.AWSCredentialsOptions, credentialSecretName, namespace, pullSecretFile string) error { diff --git a/cmd/cluster/azure/create.go b/cmd/cluster/azure/create.go index 3344ee2fb6..128d2cd4f9 100644 --- a/cmd/cluster/azure/create.go +++ b/cmd/cluster/azure/create.go @@ -84,24 +84,12 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur return fmt.Errorf("failed to deserialize infra json file: %w", err) } } else { - rhcosImage, err := lookupRHCOSImage(ctx, opts.Arch, opts.ReleaseImage, opts.PullSecretFile) + infraOpts, err := CreateInfraOptions(ctx, opts) if err != nil { - return fmt.Errorf("failed to retrieve RHCOS image: %w", err) + return err } - infra, err = (&azureinfra.CreateInfraOptions{ - Name: opts.Name, - Location: opts.AzurePlatform.Location, - InfraID: opts.InfraID, - CredentialsFile: opts.AzurePlatform.CredentialsFile, - BaseDomain: opts.BaseDomain, - RHCOSImage: rhcosImage, - VnetID: opts.AzurePlatform.VnetID, - ResourceGroupName: opts.AzurePlatform.ResourceGroupName, - NetworkSecurityGroupID: opts.AzurePlatform.NetworkSecurityGroupID, - ResourceGroupTags: opts.AzurePlatform.ResourceGroupTags, - SubnetID: opts.AzurePlatform.SubnetID, - }).Run(ctx, opts.Log) + infra, err = infraOpts.Run(ctx, opts.Log) if err != nil { return fmt.Errorf("failed to create infra: %w", err) } @@ -156,6 +144,27 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur return nil } +func CreateInfraOptions(ctx context.Context, opts *core.CreateOptions) (azureinfra.CreateInfraOptions, error) { + rhcosImage, err := lookupRHCOSImage(ctx, opts.Arch, opts.ReleaseImage, opts.PullSecretFile) + if err != nil { + return azureinfra.CreateInfraOptions{}, fmt.Errorf("failed to retrieve RHCOS image: %w", err) + } + + return azureinfra.CreateInfraOptions{ + Name: opts.Name, + Location: opts.AzurePlatform.Location, + InfraID: opts.InfraID, + CredentialsFile: opts.AzurePlatform.CredentialsFile, + BaseDomain: opts.BaseDomain, + RHCOSImage: rhcosImage, + VnetID: opts.AzurePlatform.VnetID, + ResourceGroupName: opts.AzurePlatform.ResourceGroupName, + NetworkSecurityGroupID: opts.AzurePlatform.NetworkSecurityGroupID, + ResourceGroupTags: opts.AzurePlatform.ResourceGroupTags, + SubnetID: opts.AzurePlatform.SubnetID, + }, nil +} + // lookupRHCOSImage looks up a release image and extracts the RHCOS VHD image based on the nodepool arch func lookupRHCOSImage(ctx context.Context, arch string, image string, pullSecretFile string) (string, error) { rhcosImage := "" diff --git a/cmd/cluster/cluster.go b/cmd/cluster/cluster.go index f6fb33f6b1..5a37d942ba 100644 --- a/cmd/cluster/cluster.go +++ b/cmd/cluster/cluster.go @@ -62,7 +62,8 @@ func NewCreateCommands() *cobra.Command { cmd.PersistentFlags().StringVar(&opts.ReleaseStream, "release-stream", opts.ReleaseStream, "The OCP release stream for the cluster (e.g. 4.15.0-0.nightly), this flag is ignored if release-image is set") cmd.PersistentFlags().StringVar(&opts.PullSecretFile, "pull-secret", opts.PullSecretFile, "File path to a pull secret.") cmd.PersistentFlags().StringVar(&opts.ControlPlaneAvailabilityPolicy, "control-plane-availability-policy", opts.ControlPlaneAvailabilityPolicy, "Availability policy for hosted cluster components. Supported options: SingleReplica, HighlyAvailable") - cmd.PersistentFlags().BoolVar(&opts.Render, "render", opts.Render, "Render output as YAML to stdout instead of applying") + cmd.PersistentFlags().BoolVar(&opts.Render, "render", opts.Render, "Render output as YAML instead of applying") + cmd.PersistentFlags().StringVar(&opts.RenderInto, "render-into", opts.RenderInto, "Render output as YAML into this file instead of applying. If unset, YAML will be output to stdout.") cmd.PersistentFlags().StringVar(&opts.ControlPlaneOperatorImage, "control-plane-operator-image", opts.ControlPlaneOperatorImage, "Override the default image used to deploy the control plane operator") cmd.PersistentFlags().StringVar(&opts.SSHKeyFile, "ssh-key", opts.SSHKeyFile, "Path to an SSH key file") cmd.PersistentFlags().StringVar(&opts.AdditionalTrustBundle, "additional-trust-bundle", opts.AdditionalTrustBundle, "Path to a file with user CA bundle") diff --git a/cmd/cluster/core/create.go b/cmd/cluster/core/create.go index fa714a3c23..dc353d3c55 100644 --- a/cmd/cluster/core/create.go +++ b/cmd/cluster/core/create.go @@ -62,6 +62,7 @@ type CreateOptions struct { ReleaseImage string ReleaseStream string Render bool + RenderInto string SSHKeyFile string ServiceCIDR []string ClusterCIDR []string @@ -476,7 +477,7 @@ func GetAPIServerAddressByNode(ctx context.Context, l logr.Logger) (string, erro } func Validate(ctx context.Context, opts *CreateOptions) error { - if !opts.Render { + if !opts.Render && opts.RenderInto != "" { client, err := util.GetClient() if err != nil { return err @@ -499,7 +500,7 @@ func Validate(ctx context.Context, opts *CreateOptions) error { // Validate if mgmt cluster and NodePool CPU arches don't match, a multi-arch release image or stream was used // Exception for ppc64le arch since management cluster would be in x86 and node pools are going to be in ppc64le arch - if !opts.AWSPlatform.MultiArch && !opts.Render && opts.Arch != hyperv1.ArchitecturePPC64LE { + if !opts.AWSPlatform.MultiArch && (!opts.Render || opts.RenderInto != "") && opts.Arch != hyperv1.ArchitecturePPC64LE { mgmtClusterCPUArch, err := hyperutil.GetMgmtClusterCPUArch(ctx) if err != nil { return err @@ -539,13 +540,28 @@ func CreateCluster(ctx context.Context, opts *CreateOptions, platformSpecificApp } // In render mode, print the objects and return early - if opts.Render { + if opts.Render || opts.RenderInto != "" { + output := os.Stdout + if opts.RenderInto != "" { + var err error + output, err = os.Create(opts.RenderInto) + if err != nil { + return fmt.Errorf("failed to create file for rendering output: %w", err) + } + defer func() { + if err := output.Close(); err != nil { + fmt.Printf("failed to close file for rendering output: %v\n", err) + } + }() + } for _, object := range exampleOptions.Resources().AsObjects() { - err := hyperapi.YamlSerializer.Encode(object, os.Stdout) + err := hyperapi.YamlSerializer.Encode(object, output) if err != nil { return fmt.Errorf("failed to encode objects: %w", err) } - fmt.Println("---") + if _, err := fmt.Fprintln(output, "---"); err != nil { + return fmt.Errorf("failed to write object separator: %w", err) + } } return nil } diff --git a/cmd/cluster/kubevirt/create.go b/cmd/cluster/kubevirt/create.go index 2761086733..22b18e831d 100644 --- a/cmd/cluster/kubevirt/create.go +++ b/cmd/cluster/kubevirt/create.go @@ -99,7 +99,7 @@ func ApplyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur if opts.KubevirtPlatform.ServicePublishingStrategy != NodePortServicePublishingStrategy && opts.KubevirtPlatform.APIServerAddress != "" { return fmt.Errorf("external-api-server-address is supported only for NodePort service publishing strategy, service publishing strategy %s is used", opts.KubevirtPlatform.ServicePublishingStrategy) } - if opts.KubevirtPlatform.APIServerAddress == "" && opts.KubevirtPlatform.ServicePublishingStrategy == NodePortServicePublishingStrategy && !opts.Render { + if opts.KubevirtPlatform.APIServerAddress == "" && opts.KubevirtPlatform.ServicePublishingStrategy == NodePortServicePublishingStrategy && (!opts.Render || opts.RenderInto != "") { if opts.KubevirtPlatform.APIServerAddress, err = core.GetAPIServerAddressByNode(ctx, opts.Log); err != nil { return err } diff --git a/cmd/cluster/powervs/create.go b/cmd/cluster/powervs/create.go index 8a2c6ef267..9226409c11 100644 --- a/cmd/cluster/powervs/create.go +++ b/cmd/cluster/powervs/create.go @@ -120,33 +120,8 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur } if infra == nil { - opt := &powervsinfra.CreateInfraOptions{ - Name: opts.Name, - Namespace: opts.Namespace, - BaseDomain: opts.BaseDomain, - ResourceGroup: opts.PowerVSPlatform.ResourceGroup, - InfraID: opts.InfraID, - OutputFile: opts.InfrastructureJSON, - Region: opts.PowerVSPlatform.Region, - Zone: opts.PowerVSPlatform.Zone, - CloudInstanceID: opts.PowerVSPlatform.CloudInstanceID, - CloudConnection: opts.PowerVSPlatform.CloudConnection, - VPCRegion: opts.PowerVSPlatform.VPCRegion, - VPC: opts.PowerVSPlatform.VPC, - Debug: opts.PowerVSPlatform.Debug, - RecreateSecrets: opts.PowerVSPlatform.RecreateSecrets, - PER: opts.PowerVSPlatform.PER, - TransitGatewayLocation: opts.PowerVSPlatform.TransitGatewayLocation, - TransitGateway: opts.PowerVSPlatform.TransitGateway, - } - infra = &powervsinfra.Infra{ - ID: opts.InfraID, - BaseDomain: opts.BaseDomain, - ResourceGroup: opts.PowerVSPlatform.ResourceGroup, - Region: opts.PowerVSPlatform.Region, - Zone: opts.PowerVSPlatform.Zone, - VPCRegion: opts.PowerVSPlatform.VPCRegion, - } + var opt *powervsinfra.CreateInfraOptions + opt, infra = CreateInfraOptions(opts) err = infra.SetupInfra(ctx, opt) if err != nil { return fmt.Errorf("failed to create infra: %w", err) @@ -188,3 +163,32 @@ func applyPlatformSpecificsValues(ctx context.Context, exampleOptions *apifixtur return nil } + +func CreateInfraOptions(opts *core.CreateOptions) (*powervsinfra.CreateInfraOptions, *powervsinfra.Infra) { + return &powervsinfra.CreateInfraOptions{ + Name: opts.Name, + Namespace: opts.Namespace, + BaseDomain: opts.BaseDomain, + ResourceGroup: opts.PowerVSPlatform.ResourceGroup, + InfraID: opts.InfraID, + OutputFile: opts.InfrastructureJSON, + Region: opts.PowerVSPlatform.Region, + Zone: opts.PowerVSPlatform.Zone, + CloudInstanceID: opts.PowerVSPlatform.CloudInstanceID, + CloudConnection: opts.PowerVSPlatform.CloudConnection, + VPCRegion: opts.PowerVSPlatform.VPCRegion, + VPC: opts.PowerVSPlatform.VPC, + Debug: opts.PowerVSPlatform.Debug, + RecreateSecrets: opts.PowerVSPlatform.RecreateSecrets, + PER: opts.PowerVSPlatform.PER, + TransitGatewayLocation: opts.PowerVSPlatform.TransitGatewayLocation, + TransitGateway: opts.PowerVSPlatform.TransitGateway, + }, &powervsinfra.Infra{ + ID: opts.InfraID, + BaseDomain: opts.BaseDomain, + ResourceGroup: opts.PowerVSPlatform.ResourceGroup, + Region: opts.PowerVSPlatform.Region, + Zone: opts.PowerVSPlatform.Zone, + VPCRegion: opts.PowerVSPlatform.VPCRegion, + } +} diff --git a/cmd/infra/aws/create.go b/cmd/infra/aws/create.go index ff0905bad6..64081fb31e 100644 --- a/cmd/infra/aws/create.go +++ b/cmd/infra/aws/create.go @@ -124,6 +124,11 @@ func (o *CreateInfraOptions) Run(ctx context.Context, l logr.Logger) error { if err != nil { return err } + return o.Output(result) +} + +func (o *CreateInfraOptions) Output(result *CreateInfraOutput) error { + // Write out stateful information out := os.Stdout if len(o.OutputFile) > 0 { var err error diff --git a/cmd/infra/aws/create_iam.go b/cmd/infra/aws/create_iam.go index ae7b3595a7..a98b76c27e 100644 --- a/cmd/infra/aws/create_iam.go +++ b/cmd/infra/aws/create_iam.go @@ -109,6 +109,10 @@ func (o *CreateIAMOptions) Run(ctx context.Context, client crclient.Client) erro if err != nil { return err } + return o.Output(results) +} + +func (o *CreateIAMOptions) Output(results *CreateIAMOutput) error { // Write out stateful information out := os.Stdout if len(o.OutputFile) > 0 { diff --git a/cmd/infra/powervs/create.go b/cmd/infra/powervs/create.go index 1fa2f89bef..fecb3aa2f5 100644 --- a/cmd/infra/powervs/create.go +++ b/cmd/infra/powervs/create.go @@ -276,23 +276,7 @@ func (options *CreateInfraOptions) Run(ctx context.Context) error { } defer func() { - out := os.Stdout - if len(options.OutputFile) > 0 { - var err error - out, err = os.Create(options.OutputFile) - if err != nil { - log(options.InfraID).Error(err, "cannot create output file") - } - defer out.Close() - } - outputBytes, err := json.MarshalIndent(infra, "", " ") - if err != nil { - log(options.InfraID).WithName(options.InfraID).Error(err, "failed to serialize output infra") - } - _, err = out.Write(outputBytes) - if err != nil { - log(options.InfraID).Error(err, "failed to write output infra json") - } + options.Output(infra) }() if err := infra.SetupInfra(ctx, options); err != nil { @@ -302,6 +286,26 @@ func (options *CreateInfraOptions) Run(ctx context.Context) error { return nil } +func (options *CreateInfraOptions) Output(infra *Infra) { + out := os.Stdout + if len(options.OutputFile) > 0 { + var err error + out, err = os.Create(options.OutputFile) + if err != nil { + log(options.InfraID).Error(err, "cannot create output file") + } + defer out.Close() + } + outputBytes, err := json.MarshalIndent(infra, "", " ") + if err != nil { + log(options.InfraID).WithName(options.InfraID).Error(err, "failed to serialize output infra") + } + _, err = out.Write(outputBytes) + if err != nil { + log(options.InfraID).Error(err, "failed to write output infra json") + } +} + // checkUnsupportedPowerVSZone omitting powervs zones that does not support hypershift infra creation flow func checkUnsupportedPowerVSZone(zone string) error { for i := 0; i < len(unsupportedPowerVSZones); i++ { diff --git a/product-cli/cmd/cluster/cluster.go b/product-cli/cmd/cluster/cluster.go index ac7f2bb56b..9e91744181 100644 --- a/product-cli/cmd/cluster/cluster.go +++ b/product-cli/cmd/cluster/cluster.go @@ -70,7 +70,8 @@ func NewCreateCommands() *cobra.Command { cmd.PersistentFlags().StringVar(&opts.NetworkType, "network-type", opts.NetworkType, "Enum specifying the cluster SDN provider. Supports either Calico, OVNKubernetes, OpenShiftSDN or Other.") cmd.PersistentFlags().StringVar(&opts.PullSecretFile, "pull-secret", opts.PullSecretFile, "Filepath to a pull secret.") cmd.PersistentFlags().StringVar(&opts.ReleaseImage, "release-image", opts.ReleaseImage, "The OCP release image for the HostedCluster.") - cmd.PersistentFlags().BoolVar(&opts.Render, "render", opts.Render, "Renders the HostedCluster manifest output as YAML to stdout instead of automatically applying the manifests to the management cluster.") + cmd.PersistentFlags().BoolVar(&opts.Render, "render", opts.Render, "Render output as YAML instead of applying") + cmd.PersistentFlags().StringVar(&opts.RenderInto, "render-into", opts.RenderInto, "Render output as YAML into this file instead of applying. If unset, YAML will be output to stdout.") cmd.PersistentFlags().StringArrayVar(&opts.ServiceCIDR, "service-cidr", opts.ServiceCIDR, "The CIDR of the service network. Can be specified multiple times.") cmd.PersistentFlags().BoolVar(&opts.DefaultDual, "default-dual", opts.DefaultDual, "Defines the Service and Cluster CIDRs as dual-stack default values. This flag is ignored if service-cidr or cluster-cidr are set. Cannot be defined with service-cidr or cluster-cidr flag.") cmd.PersistentFlags().StringVar(&opts.SSHKeyFile, "ssh-key", opts.SSHKeyFile, "Filepath to an SSH key file.") diff --git a/test/e2e/util/fixture.go b/test/e2e/util/fixture.go index 6f446cf966..8950c61608 100644 --- a/test/e2e/util/fixture.go +++ b/test/e2e/util/fixture.go @@ -19,6 +19,7 @@ import ( "github.com/openshift/hypershift/cmd/cluster/none" "github.com/openshift/hypershift/cmd/cluster/powervs" awsutil "github.com/openshift/hypershift/cmd/infra/aws/util" + "github.com/openshift/hypershift/cmd/util" "github.com/openshift/hypershift/test/e2e/util/dump" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/wait" @@ -47,23 +48,82 @@ func createClusterOpts(ctx context.Context, client crclient.Client, hc *hyperv1. // createCluster calls the correct cluster create CLI function based on the // cluster platform. -func createCluster(ctx context.Context, hc *hyperv1.HostedCluster, opts *core.CreateOptions) error { +func createCluster(ctx context.Context, hc *hyperv1.HostedCluster, opts *core.CreateOptions, outputDir string) error { + infraFile := filepath.Join(outputDir, "infrastructure.json") + iamFile := filepath.Join(outputDir, "iam.json") + manifestsFile := filepath.Join(outputDir, "manifests.yaml") switch hc.Spec.Platform.Type { case hyperv1.AWSPlatform: - return aws.CreateCluster(ctx, opts) + infraOpts := aws.CreateInfraOptions(opts) + infraOpts.OutputFile = infraFile + infra, err := infraOpts.CreateInfra(ctx, opts.Log) + if err != nil { + return fmt.Errorf("failed to create infra: %w", err) + } + if err := infraOpts.Output(infra); err != nil { + return fmt.Errorf("failed to write infra: %w", err) + } + + client, err := util.GetClient() + if err != nil { + return err + } + iamOpts := aws.CreateIAMOptions(opts, infra) + iamOpts.OutputFile = iamFile + iam, err := iamOpts.CreateIAM(ctx, client) + if err != nil { + return fmt.Errorf("failed to create IAM: %w", err) + } + if err := iamOpts.Output(iam); err != nil { + return fmt.Errorf("failed to write IAM: %w", err) + } + + opts.InfrastructureJSON = infraFile + opts.AWSPlatform.IAMJSON = iamFile + return renderCreate(ctx, opts, manifestsFile, aws.CreateCluster) case hyperv1.NonePlatform: - return none.CreateCluster(ctx, opts) + return renderCreate(ctx, opts, manifestsFile, none.CreateCluster) case hyperv1.KubevirtPlatform: - return kubevirt.CreateCluster(ctx, opts) + return renderCreate(ctx, opts, manifestsFile, kubevirt.CreateCluster) case hyperv1.AzurePlatform: - return azure.CreateCluster(ctx, opts) + infraOpts, err := azure.CreateInfraOptions(ctx, opts) + if err != nil { + return fmt.Errorf("failed to create infra options: %w", err) + } + infraOpts.OutputFile = infraFile + if _, err := infraOpts.Run(ctx, opts.Log); err != nil { + return fmt.Errorf("failed to create infra: %w", err) + } + + opts.InfrastructureJSON = infraFile + return renderCreate(ctx, opts, manifestsFile, azure.CreateCluster) case hyperv1.PowerVSPlatform: - return powervs.CreateCluster(ctx, opts) + infraOpts, infra := powervs.CreateInfraOptions(opts) + infraOpts.OutputFile = infraFile + if err := infra.SetupInfra(ctx, infraOpts); err != nil { + return fmt.Errorf("failed to setup infra: %w", err) + } + infraOpts.Output(infra) + + opts.InfrastructureJSON = infraFile + return renderCreate(ctx, opts, manifestsFile, powervs.CreateCluster) default: return fmt.Errorf("unsupported platform %s", hc.Spec.Platform.Type) } } +func renderCreate(ctx context.Context, opts *core.CreateOptions, outputFile string, create func(context.Context, *core.CreateOptions) error) error { + opts.Render = true + opts.RenderInto = outputFile + if err := create(ctx, opts); err != nil { + return fmt.Errorf("failed to render cluster manifests: %w", err) + } + + opts.Render = false + opts.RenderInto = "" + return create(ctx, opts) +} + // destroyCluster calls the correct cluster destroy CLI function based on the // cluster platform and the options used to create the cluster. func destroyCluster(ctx context.Context, t *testing.T, hc *hyperv1.HostedCluster, createOpts *core.CreateOptions) error { @@ -209,26 +269,25 @@ func newClusterDumper(hc *hyperv1.HostedCluster, opts *core.CreateOptions, artif t.Logf("Skipping cluster dump because no artifact directory was provided") return nil } - dumpDir := filepath.Join(artifactDir, strings.ReplaceAll(t.Name(), "/", "_")) switch hc.Spec.Platform.Type { case hyperv1.AWSPlatform: var dumpErrors []error - err := dump.DumpMachineConsoleLogs(ctx, hc, opts.AWSPlatform.AWSCredentialsOpts, dumpDir) + err := dump.DumpMachineConsoleLogs(ctx, hc, opts.AWSPlatform.AWSCredentialsOpts, artifactDir) if err != nil { t.Logf("Failed saving machine console logs; this is nonfatal: %v", err) } - err = dump.DumpHostedCluster(ctx, t, hc, dumpGuestCluster, dumpDir) + err = dump.DumpHostedCluster(ctx, t, hc, dumpGuestCluster, artifactDir) if err != nil { dumpErrors = append(dumpErrors, fmt.Errorf("failed to dump hosted cluster: %w", err)) } - err = dump.DumpJournals(t, ctx, hc, dumpDir, opts.AWSPlatform.AWSCredentialsOpts.AWSCredentialsFile) + err = dump.DumpJournals(t, ctx, hc, artifactDir, opts.AWSPlatform.AWSCredentialsOpts.AWSCredentialsFile) if err != nil { t.Logf("Failed to dump machine journals; this is nonfatal: %v", err) } return utilerrors.NewAggregate(dumpErrors) default: - err := dump.DumpHostedCluster(ctx, t, hc, dumpGuestCluster, dumpDir) + err := dump.DumpHostedCluster(ctx, t, hc, dumpGuestCluster, artifactDir) if err != nil { return fmt.Errorf("failed to dump hosted cluster: %w", err) } @@ -236,3 +295,7 @@ func newClusterDumper(hc *hyperv1.HostedCluster, opts *core.CreateOptions, artif } } } + +func artifactSubdirFor(t *testing.T) string { + return strings.ReplaceAll(t.Name(), "/", "_") +} diff --git a/test/e2e/util/hypershift_framework.go b/test/e2e/util/hypershift_framework.go index 86875bc2c2..bcda1414f3 100644 --- a/test/e2e/util/hypershift_framework.go +++ b/test/e2e/util/hypershift_framework.go @@ -3,6 +3,8 @@ package util import ( "context" "fmt" + "os" + "path/filepath" "runtime/debug" "strings" "testing" @@ -50,8 +52,10 @@ func NewHypershiftTest(t *testing.T, ctx context.Context, test hypershiftTestFun } func (h *hypershiftTest) Execute(opts *core.CreateOptions, platform hyperv1.PlatformType, artifactDir string, serviceAccountSigningKey []byte) { + artifactDir = filepath.Join(artifactDir, artifactSubdirFor(h.T)) + // create a hypershift cluster for the test - hostedCluster := h.createHostedCluster(opts, platform, serviceAccountSigningKey) + hostedCluster := h.createHostedCluster(opts, platform, serviceAccountSigningKey, artifactDir) // if cluster creation failed, immediately try and clean up. if h.Failed() { @@ -178,7 +182,7 @@ func (h *hypershiftTest) postTeardown(hostedCluster *hyperv1.HostedCluster, opts }) } -func (h *hypershiftTest) createHostedCluster(opts *core.CreateOptions, platform hyperv1.PlatformType, serviceAccountSigningKey []byte) *hyperv1.HostedCluster { +func (h *hypershiftTest) createHostedCluster(opts *core.CreateOptions, platform hyperv1.PlatformType, serviceAccountSigningKey []byte, artifactDir string) *hyperv1.HostedCluster { h.Logf("createHostedCluster()") g := NewWithT(h.T) @@ -258,9 +262,16 @@ func (h *hypershiftTest) createHostedCluster(opts *core.CreateOptions, platform opts, err = createClusterOpts(h.ctx, h.client, hc, opts) g.Expect(err).NotTo(HaveOccurred(), "failed to generate platform specific cluster options") + // Dump the output from rendering the cluster objects for posterity + if err := os.MkdirAll(artifactDir, 0755); err != nil { + h.Errorf("failed to create dump directory: %v", err) + } + // Try and create the cluster. If it fails, mark test as failed and return. + opts.Render = false + opts.RenderInto = "" h.Logf("Creating a new cluster. Options: %v", opts) - if err := createCluster(h.ctx, hc, opts); err != nil { + if err := createCluster(h.ctx, hc, opts, artifactDir); err != nil { h.Errorf("failed to create cluster, tearing down: %v", err) return hc }