diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index c11f8c60..939b99e9 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -512,12 +512,15 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) { stateBag.Put(constants.ArmKeepOSDisk, b.config.KeepOSDisk) stateBag.Put(constants.ArmIsSIGImage, b.config.isPublishToSIG()) + // Set Specialized as false so that we can pull it from the state later even if we're not publishing to SIG + stateBag.Put(constants.ArmSharedImageGalleryDestinationSpecialized, false) if b.config.isPublishToSIG() { stateBag.Put(constants.ArmManagedImageSigPublishResourceGroup, b.config.SharedGalleryDestination.SigDestinationResourceGroup) stateBag.Put(constants.ArmManagedImageSharedGalleryName, b.config.SharedGalleryDestination.SigDestinationGalleryName) stateBag.Put(constants.ArmManagedImageSharedGalleryImageName, b.config.SharedGalleryDestination.SigDestinationImageName) stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersion, b.config.SharedGalleryDestination.SigDestinationImageVersion) stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionStorageAccountType, b.config.SharedGalleryDestination.SigDestinationStorageAccountType) + stateBag.Put(constants.ArmSharedImageGalleryDestinationSpecialized, b.config.SharedGalleryDestination.SigDestinationSpecialized) stateBag.Put(constants.ArmManagedImageSubscription, b.config.ClientConfig.SubscriptionID) stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionEndOfLifeDate, b.config.SharedGalleryImageVersionEndOfLifeDate) stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionReplicaCount, b.config.SharedGalleryImageVersionReplicaCount) diff --git a/builder/azure/arm/config.go b/builder/azure/arm/config.go index 13ea9125..5e3faf7a 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -106,6 +106,8 @@ type SharedImageGalleryDestination struct { // Specify a storage account type for the Shared Image Gallery Image Version. // Defaults to `Standard_LRS`. Accepted values are `Standard_LRS`, `Standard_ZRS` and `Premium_LRS` SigDestinationStorageAccountType string `mapstructure:"storage_account_type"` + // Set to true if publishing to a Specialized Gallery, this skips a call to set the build VM's OS state as Generalized + SigDestinationSpecialized bool `mapstructure:"specialized"` } type Spot struct { @@ -1094,8 +1096,13 @@ func assertRequiredParametersSet(c *Config, errs *packersdk.MultiError) { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("A managed image must be created from a managed image, it cannot be created from a VHD.")) } - if (c.SecureBootEnabled || c.VTpmEnabled) && (c.ManagedImageName != "" || c.ManagedImageResourceGroupName != "") { - errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("A managed image (managed_image_name, managed_image_resource_group_name) can not set SecureBoot or VTpm, these features are only supported when directly publishing to a Shared Image Gallery")) + if c.ManagedImageName != "" || c.ManagedImageResourceGroupName != "" { + if c.SecureBootEnabled || c.VTpmEnabled { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("A managed image (managed_image_name, managed_image_resource_group_name) can not set SecureBoot or VTpm, these features are only supported when directly publishing to a Shared Image Gallery")) + } + if c.SharedGalleryDestination.SigDestinationSpecialized { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("A managed image (managed_image_name, managed_image_resource_group_name) can not be Specialized (shared_image_gallery_destination.specialized can not be set), Specialized images are only supported when directly publishing to a Shared Image Gallery")) + } } if (c.CaptureContainerName != "" || c.CaptureNamePrefix != "" || c.ManagedImageName != "") && c.DiskEncryptionSetId != "" { diff --git a/builder/azure/arm/config.hcl2spec.go b/builder/azure/arm/config.hcl2spec.go index c2fb1df0..1826ae35 100644 --- a/builder/azure/arm/config.hcl2spec.go +++ b/builder/azure/arm/config.hcl2spec.go @@ -364,6 +364,7 @@ type FlatSharedImageGalleryDestination struct { SigDestinationImageVersion *string `mapstructure:"image_version" cty:"image_version" hcl:"image_version"` SigDestinationReplicationRegions []string `mapstructure:"replication_regions" cty:"replication_regions" hcl:"replication_regions"` SigDestinationStorageAccountType *string `mapstructure:"storage_account_type" cty:"storage_account_type" hcl:"storage_account_type"` + SigDestinationSpecialized *bool `mapstructure:"specialized" cty:"specialized" hcl:"specialized"` } // FlatMapstructure returns a new FlatSharedImageGalleryDestination. @@ -385,6 +386,7 @@ func (*FlatSharedImageGalleryDestination) HCL2Spec() map[string]hcldec.Spec { "image_version": &hcldec.AttrSpec{Name: "image_version", Type: cty.String, Required: false}, "replication_regions": &hcldec.AttrSpec{Name: "replication_regions", Type: cty.List(cty.String), Required: false}, "storage_account_type": &hcldec.AttrSpec{Name: "storage_account_type", Type: cty.String, Required: false}, + "specialized": &hcldec.AttrSpec{Name: "specialized", Type: cty.Bool, Required: false}, } return s } diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index cd400b33..d01791c5 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -1380,6 +1380,36 @@ func TestConfigShouldRejectVTPMWhenPublishingToAManagedImage(t *testing.T) { } } +func TestConfigShouldRejectSpecializedWhenPublishingManagedImage(t *testing.T) { + expectedErrorMessage := "A managed image (managed_image_name, managed_image_resource_group_name) can not be Specialized (shared_image_gallery_destination.specialized can not be set), Specialized images are only supported when directly publishing to a Shared Image Gallery" + config := map[string]interface{}{ + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "subscription_id": "ignore", + "communicator": "none", + "managed_image_resource_group_name": "ignore", + "managed_image_name": "ignore", + "shared_image_gallery_destination": map[string]interface{}{ + "resource_group": "ignore", + "gallery_name": "ignore", + "image_name": "ignore", + "image_version": "1.0.0", + "specialized": "true", + }, + // Does not matter for this test case, just pick one. + "os_type": constants.Target_Linux, + } + + var c Config + _, err := c.Prepare(config, getPackerConfiguration()) + if err == nil { + t.Fatal("expected config to reject managed image with secure boot, secure boot is only allowed when direct publishing to SIG") + } else if !strings.Contains(err.Error(), expectedErrorMessage) { + t.Fatalf("unexpected rejection reason, expected %s to contain %s", err.Error(), expectedErrorMessage) + } +} func TestConfigShouldAcceptPlatformManagedImageBuild(t *testing.T) { config := map[string]interface{}{ "image_offer": "ignore", diff --git a/builder/azure/arm/step_capture_image.go b/builder/azure/arm/step_capture_image.go index 06a7f8a2..25fbc00a 100644 --- a/builder/azure/arm/step_capture_image.go +++ b/builder/azure/arm/step_capture_image.go @@ -69,7 +69,6 @@ func (s *StepCaptureImage) captureImage(ctx context.Context, resourceGroupName s } func (s *StepCaptureImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - s.say("Generalizing machine ...") var computeName = state.Get(constants.ArmComputeName).(string) var location = state.Get(constants.ArmLocation).(string) @@ -79,12 +78,18 @@ func (s *StepCaptureImage) Run(ctx context.Context, state multistep.StateBag) mu var isManagedImage = state.Get(constants.ArmIsManagedImage).(bool) var isSIGImage = state.Get(constants.ArmIsSIGImage).(bool) - + var skipGeneralization = state.Get(constants.ArmSharedImageGalleryDestinationSpecialized).(bool) s.say(fmt.Sprintf(" -> Compute ResourceGroupName : '%s'", resourceGroupName)) s.say(fmt.Sprintf(" -> Compute Name : '%s'", computeName)) s.say(fmt.Sprintf(" -> Compute Location : '%s'", location)) - err := s.generalizeVM(resourceGroupName, computeName) + var err error + if skipGeneralization { + s.say("Skipping generalization of Compute Gallery Image") + } else { + s.say("Generalizing machine ...") + err = s.generalizeVM(resourceGroupName, computeName) + } if err == nil { if isManagedImage { diff --git a/builder/azure/arm/step_capture_image_test.go b/builder/azure/arm/step_capture_image_test.go index 9117d3ce..e431f926 100644 --- a/builder/azure/arm/step_capture_image_test.go +++ b/builder/azure/arm/step_capture_image_test.go @@ -65,6 +65,66 @@ func TestStepCaptureImageShouldPassIfCapturePasses(t *testing.T) { } } +func TestStepCaptureImageShouldCallGeneralizeIfSpecializedIsFalse(t *testing.T) { + generalizeCount := 0 + var testSubject = &StepCaptureImage{ + captureVhd: func(context.Context, string, string, *compute.VirtualMachineCaptureParameters) error { return nil }, + generalizeVM: func(string, string) error { + generalizeCount++ + return nil + }, + get: func(client *AzureClient) *CaptureTemplate { + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCaptureImage() + stateBag.Put(constants.ArmSharedImageGalleryDestinationSpecialized, false) + var result = testSubject.Run(context.Background(), stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } + if generalizeCount != 1 { + t.Fatalf("Expected generalize to be called 1, was called %d times", generalizeCount) + } +} + +func TestStepCaptureImageShouldNotCallGeneralizeIfSpecializedIsTrue(t *testing.T) { + generalizeCount := 0 + var testSubject = &StepCaptureImage{ + captureVhd: func(context.Context, string, string, *compute.VirtualMachineCaptureParameters) error { return nil }, + generalizeVM: func(string, string) error { + generalizeCount++ + return nil + }, + get: func(client *AzureClient) *CaptureTemplate { + return nil + }, + say: func(message string) {}, + error: func(e error) {}, + } + + stateBag := createTestStateBagStepCaptureImage() + stateBag.Put(constants.ArmSharedImageGalleryDestinationSpecialized, true) + var result = testSubject.Run(context.Background(), stateBag) + if result != multistep.ActionContinue { + t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) + } + + if _, ok := stateBag.GetOk(constants.Error); ok == true { + t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) + } + if generalizeCount != 0 { + t.Fatalf("Expected generalize to not be called, was called %d times", generalizeCount) + } +} + func TestStepCaptureImageShouldTakeStepArgumentsFromStateBag(t *testing.T) { cancelCh := make(chan<- struct{}) defer close(cancelCh) @@ -136,6 +196,7 @@ func createTestStateBagStepCaptureImage() multistep.StateBag { stateBag.Put(constants.ArmManagedImageName, "") stateBag.Put(constants.ArmImageParameters, &compute.Image{}) stateBag.Put(constants.ArmIsSIGImage, false) + stateBag.Put(constants.ArmSharedImageGalleryDestinationSpecialized, false) return stateBag } diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go index 6859ee6e..63ee6b54 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -54,6 +54,7 @@ const ( ArmManagedImageSharedGalleryImageVersionReplicaCount string = "arm.ArmManagedImageSharedGalleryImageVersionReplicaCount" ArmManagedImageSharedGalleryImageVersionExcludeFromLatest string = "arm.ArmManagedImageSharedGalleryImageVersionExcludeFromLatest" ArmManagedImageSharedGalleryImageVersionStorageAccountType string = "arm.ArmManagedImageSharedGalleryImageVersionStorageAccountType" + ArmSharedImageGalleryDestinationSpecialized string = "arm.ArmSharedImageGalleryDestinationSpecialized" ArmManagedImageSubscription string = "arm.ArmManagedImageSubscription" ArmAsyncResourceGroupDelete string = "arm.AsyncResourceGroupDelete" ArmManagedImageOSDiskSnapshotName string = "arm.ManagedImageOSDiskSnapshotName" diff --git a/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx b/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx index f6ac78d6..dc5fb0cb 100644 --- a/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx +++ b/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx @@ -15,4 +15,6 @@ - `storage_account_type` (string) - Specify a storage account type for the Shared Image Gallery Image Version. Defaults to `Standard_LRS`. Accepted values are `Standard_LRS`, `Standard_ZRS` and `Premium_LRS` +- `specialized` (bool) - Set to true if publishing to a Specialized Gallery, this skips a call to set the resulting VM's OS state as Generalized +