diff --git a/.web-docs/components/builder/arm/README.md b/.web-docs/components/builder/arm/README.md index 9f730dd4..e7bc3f4b 100644 --- a/.web-docs/components/builder/arm/README.md +++ b/.web-docs/components/builder/arm/README.md @@ -227,9 +227,9 @@ Providing `temp_resource_group_name` or `location` in combination with - `shared_gallery_image_version_end_of_life_date` (string) - The end of life date (2006-01-02T15:04:05.99Z) of the gallery Image Version. This property can be used for decommissioning purposes. -- `shared_image_gallery_replica_count` (int32) - The number of replicas of the Image Version to be created per region. This - property would take effect for a region when regionalReplicaCount is not specified. +- `shared_image_gallery_replica_count` (int64) - The number of replicas of the Image Version to be created per region. Replica count must be between 1 and 100, but 50 replicas should be sufficient for most use cases. + When using shallow replication `use_shallow_replication=true` the value can only be 1. - `shared_gallery_image_version_exclude_from_latest` (bool) - If set to true, Virtual Machines deployed from the latest version of the Image Definition won't use this Image Version. @@ -640,13 +640,17 @@ The shared_image_gallery_destination block is available for publishing a new ima - `image_version` (string) - Sig Destination Image Version -- `replication_regions` ([]string) - Sig Destination Replication Regions +- `replication_regions` ([]string) - A list of regions to replicate the image version in, by default the build location will be used as a replication region (the build location is either set in the location field, or the location of the resource group used in `build_resource_group_name` will be included. + Can not contain any region but the build region when using shallow replication - `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 build VM's OS state as Generalized +- `use_shallow_replication` (bool) - Setting a `shared_image_gallery_replica_count` or any `replication_regions` is unnecessary for shallow builds, as they can only replicate to the build region and must have a replica count of 1 + Refer to [Shallow Replication](https://learn.microsoft.com/en-us/azure/virtual-machines/shared-image-galleries?tabs=azure-cli#shallow-replication) for details on when to use shallow replication mode. + diff --git a/builder/azure/arm/builder.go b/builder/azure/arm/builder.go index aedbd698..d3e51971 100644 --- a/builder/azure/arm/builder.go +++ b/builder/azure/arm/builder.go @@ -213,21 +213,30 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) return nil, fmt.Errorf("a gallery image version for image name:version %s:%s already exists in gallery %s", b.config.SharedGalleryDestination.SigDestinationImageName, b.config.SharedGalleryDestination.SigDestinationImageVersion, b.config.SharedGalleryDestination.SigDestinationGalleryName) } - // SIG requires that replication regions include the region in which the Managed Image resides - managedImageLocation := normalizeAzureRegion(b.stateBag.Get(constants.ArmLocation).(string)) + // SIG requires that replication regions include the region in which the created image version resides + buildLocation := normalizeAzureRegion(b.stateBag.Get(constants.ArmLocation).(string)) foundMandatoryReplicationRegion := false var normalizedReplicationRegions []string for _, region := range b.config.SharedGalleryDestination.SigDestinationReplicationRegions { // change region to lower-case and strip spaces normalizedRegion := normalizeAzureRegion(region) normalizedReplicationRegions = append(normalizedReplicationRegions, normalizedRegion) - if strings.EqualFold(normalizedRegion, managedImageLocation) { + if strings.EqualFold(normalizedRegion, buildLocation) { foundMandatoryReplicationRegion = true continue } } if foundMandatoryReplicationRegion == false { - b.config.SharedGalleryDestination.SigDestinationReplicationRegions = append(normalizedReplicationRegions, managedImageLocation) + b.config.SharedGalleryDestination.SigDestinationReplicationRegions = append(normalizedReplicationRegions, buildLocation) + } + // TODO It would be better if validation could be handled in a central location + // Currently we rely on the build Resource Group being queried if used to get the build location + // So we have to do this validation afterwards + // We should remove this logic builder and handle this logic via the `Step` pattern + if b.config.SharedGalleryDestination.SigDestinationUseShallowReplicationMode { + if len(b.config.SharedGalleryDestination.SigDestinationReplicationRegions) != 1 { + return nil, fmt.Errorf("when `use_shallow_replication` is enabled the value of `replicated_regions` must match the build region specified by `location` or match the region of `build_resource_group_name`.") + } } b.stateBag.Put(constants.ArmManagedImageSharedGalleryReplicationRegions, b.config.SharedGalleryDestination.SigDestinationReplicationRegions) } @@ -534,6 +543,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) { 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.ArmSharedImageGalleryDestinationShallowReplication, b.config.SharedGalleryDestination.SigDestinationUseShallowReplicationMode) 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 8303af68..dab26842 100644 --- a/builder/azure/arm/config.go +++ b/builder/azure/arm/config.go @@ -97,17 +97,24 @@ type SharedImageGallery struct { } type SharedImageGalleryDestination struct { - SigDestinationSubscription string `mapstructure:"subscription"` - SigDestinationResourceGroup string `mapstructure:"resource_group"` - SigDestinationGalleryName string `mapstructure:"gallery_name"` - SigDestinationImageName string `mapstructure:"image_name"` - SigDestinationImageVersion string `mapstructure:"image_version"` + SigDestinationSubscription string `mapstructure:"subscription"` + SigDestinationResourceGroup string `mapstructure:"resource_group"` + SigDestinationGalleryName string `mapstructure:"gallery_name"` + SigDestinationImageName string `mapstructure:"image_name"` + SigDestinationImageVersion string `mapstructure:"image_version"` + // A list of regions to replicate the image version in, by default the build location will be used as a replication region (the build location is either set in the location field, or the location of the resource group used in `build_resource_group_name` will be included. + // Can not contain any region but the build region when using shallow replication SigDestinationReplicationRegions []string `mapstructure:"replication_regions"` // 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"` + // Set to true to use shallow replication mode, which will publish the image version without replication. This option results in a faster build, but the image version's replication count and regions are not modifiable builds with shallow replication enabled. + + // Setting a `shared_image_gallery_replica_count` or any `replication_regions` is unnecessary for shallow builds, as they can only replicate to the build region and must have a replica count of 1 + // Refer to [Shallow Replication](https://learn.microsoft.com/en-us/azure/virtual-machines/shared-image-galleries?tabs=azure-cli#shallow-replication) for details on when to use shallow replication mode. + SigDestinationUseShallowReplicationMode bool `mapstructure:"use_shallow_replication" required:"false"` } type Spot struct { @@ -212,10 +219,10 @@ type Config struct { // The end of life date (2006-01-02T15:04:05.99Z) of the gallery Image Version. This property // can be used for decommissioning purposes. SharedGalleryImageVersionEndOfLifeDate string `mapstructure:"shared_gallery_image_version_end_of_life_date" required:"false"` - // The number of replicas of the Image Version to be created per region. This - // property would take effect for a region when regionalReplicaCount is not specified. + // The number of replicas of the Image Version to be created per region. // Replica count must be between 1 and 100, but 50 replicas should be sufficient for most use cases. - SharedGalleryImageVersionReplicaCount int32 `mapstructure:"shared_image_gallery_replica_count" required:"false"` + // When using shallow replication `use_shallow_replication=true` the value can only be 1. + SharedGalleryImageVersionReplicaCount int64 `mapstructure:"shared_image_gallery_replica_count" required:"false"` // If set to true, Virtual Machines deployed from the latest version of the // Image Definition won't use this Image Version. SharedGalleryImageVersionExcludeFromLatest bool `mapstructure:"shared_gallery_image_version_exclude_from_latest" required:"false"` @@ -1256,6 +1263,15 @@ func assertRequiredParametersSet(c *Config, errs *packersdk.MultiError) { if c.SharedGalleryDestination.SigDestinationSubscription == "" { c.SharedGalleryDestination.SigDestinationSubscription = c.ClientConfig.SubscriptionID } + if c.SharedGalleryDestination.SigDestinationUseShallowReplicationMode { + if c.SharedGalleryImageVersionReplicaCount == 0 { + c.SharedGalleryImageVersionReplicaCount = 1 + } + + if c.SharedGalleryImageVersionReplicaCount != 1 { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("When using shallow replication the replica count can only be 1, leaving this value unset will default to 1")) + } + } } if c.SharedGalleryTimeout == 0 { // default to a one-hour timeout. In the sdk, the default is 15 m. diff --git a/builder/azure/arm/config.hcl2spec.go b/builder/azure/arm/config.hcl2spec.go index dd734d7a..ab737d3a 100644 --- a/builder/azure/arm/config.hcl2spec.go +++ b/builder/azure/arm/config.hcl2spec.go @@ -39,7 +39,7 @@ type FlatConfig struct { SharedGalleryDestination *FlatSharedImageGalleryDestination `mapstructure:"shared_image_gallery_destination" cty:"shared_image_gallery_destination" hcl:"shared_image_gallery_destination"` SharedGalleryTimeout *string `mapstructure:"shared_image_gallery_timeout" cty:"shared_image_gallery_timeout" hcl:"shared_image_gallery_timeout"` SharedGalleryImageVersionEndOfLifeDate *string `mapstructure:"shared_gallery_image_version_end_of_life_date" required:"false" cty:"shared_gallery_image_version_end_of_life_date" hcl:"shared_gallery_image_version_end_of_life_date"` - SharedGalleryImageVersionReplicaCount *int32 `mapstructure:"shared_image_gallery_replica_count" required:"false" cty:"shared_image_gallery_replica_count" hcl:"shared_image_gallery_replica_count"` + SharedGalleryImageVersionReplicaCount *int64 `mapstructure:"shared_image_gallery_replica_count" required:"false" cty:"shared_image_gallery_replica_count" hcl:"shared_image_gallery_replica_count"` SharedGalleryImageVersionExcludeFromLatest *bool `mapstructure:"shared_gallery_image_version_exclude_from_latest" required:"false" cty:"shared_gallery_image_version_exclude_from_latest" hcl:"shared_gallery_image_version_exclude_from_latest"` ImagePublisher *string `mapstructure:"image_publisher" required:"true" cty:"image_publisher" hcl:"image_publisher"` ImageOffer *string `mapstructure:"image_offer" required:"true" cty:"image_offer" hcl:"image_offer"` @@ -361,14 +361,15 @@ func (*FlatSharedImageGallery) HCL2Spec() map[string]hcldec.Spec { // FlatSharedImageGalleryDestination is an auto-generated flat version of SharedImageGalleryDestination. // Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. type FlatSharedImageGalleryDestination struct { - SigDestinationSubscription *string `mapstructure:"subscription" cty:"subscription" hcl:"subscription"` - SigDestinationResourceGroup *string `mapstructure:"resource_group" cty:"resource_group" hcl:"resource_group"` - SigDestinationGalleryName *string `mapstructure:"gallery_name" cty:"gallery_name" hcl:"gallery_name"` - SigDestinationImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"` - 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"` + SigDestinationSubscription *string `mapstructure:"subscription" cty:"subscription" hcl:"subscription"` + SigDestinationResourceGroup *string `mapstructure:"resource_group" cty:"resource_group" hcl:"resource_group"` + SigDestinationGalleryName *string `mapstructure:"gallery_name" cty:"gallery_name" hcl:"gallery_name"` + SigDestinationImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"` + 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"` + SigDestinationUseShallowReplicationMode *bool `mapstructure:"use_shallow_replication" required:"false" cty:"use_shallow_replication" hcl:"use_shallow_replication"` } // FlatMapstructure returns a new FlatSharedImageGalleryDestination. @@ -383,14 +384,15 @@ func (*SharedImageGalleryDestination) FlatMapstructure() interface{ HCL2Spec() m // The decoded values from this spec will then be applied to a FlatSharedImageGalleryDestination. func (*FlatSharedImageGalleryDestination) HCL2Spec() map[string]hcldec.Spec { s := map[string]hcldec.Spec{ - "subscription": &hcldec.AttrSpec{Name: "subscription", Type: cty.String, Required: false}, - "resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false}, - "gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false}, - "image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false}, - "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}, + "subscription": &hcldec.AttrSpec{Name: "subscription", Type: cty.String, Required: false}, + "resource_group": &hcldec.AttrSpec{Name: "resource_group", Type: cty.String, Required: false}, + "gallery_name": &hcldec.AttrSpec{Name: "gallery_name", Type: cty.String, Required: false}, + "image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false}, + "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}, + "use_shallow_replication": &hcldec.AttrSpec{Name: "use_shallow_replication", Type: cty.Bool, Required: false}, } return s } diff --git a/builder/azure/arm/config_test.go b/builder/azure/arm/config_test.go index 99c7fd30..162c5d79 100644 --- a/builder/azure/arm/config_test.go +++ b/builder/azure/arm/config_test.go @@ -1283,6 +1283,85 @@ func TestConfigShouldAcceptAbsentManagedImageButPresentSharedImageGalleryDestina } } +func TestConfigShouldRejectShallowReplicationWithInvalidReplicationCount(t *testing.T) { + config := map[string]interface{}{ + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "subscription_id": "ignore", + "communicator": "none", + "os_type": constants.Target_Linux, + "shared_image_gallery_replica_count": "2", + "shared_image_gallery_destination": map[string]string{ + "resource_group": "ignore", + "gallery_name": "ignore", + "image_name": "ignore", + "image_version": "1.0.1", + "replication_regions": "ignore", + "use_shallow_replication": "true", + }, + } + expectedErrorMessage := "When using shallow replication the replica count can only be 1, leaving this value unset will default to 1" + var c Config + _, err := c.Prepare(config, getPackerConfiguration()) + if err == nil { + t.Fatalf("expected config to reject invalid replica count using shallow replication but it was accepted") + } else if !strings.Contains(err.Error(), expectedErrorMessage) { + t.Fatalf("unexpected rejection reason, expected %s to contain %s", err.Error(), expectedErrorMessage) + } +} + +func TestConfigShouldAcceptShallowReplicationWithReplicaCount(t *testing.T) { + config := map[string]interface{}{ + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "subscription_id": "ignore", + "communicator": "none", + "os_type": constants.Target_Linux, + "shared_image_gallery_replica_count": "1", + "shared_image_gallery_destination": map[string]string{ + "resource_group": "ignore", + "gallery_name": "ignore", + "image_name": "ignore", + "image_version": "1.0.1", + "replication_regions": "ignore", + "use_shallow_replication": "true", + }, + } + var c Config + _, err := c.Prepare(config, getPackerConfiguration()) + if err != nil { + t.Fatalf("expected config to accept shallow replication with set replica count (1) build: %v", err) + } +} + +func TestConfigShouldAcceptShallowReplicationWithWithUnsetReplicaCount(t *testing.T) { + config := map[string]interface{}{ + "image_offer": "ignore", + "image_publisher": "ignore", + "image_sku": "ignore", + "location": "ignore", + "subscription_id": "ignore", + "communicator": "none", + "os_type": constants.Target_Linux, + "shared_image_gallery_destination": map[string]string{ + "resource_group": "ignore", + "gallery_name": "ignore", + "image_name": "ignore", + "image_version": "1.0.1", + "replication_regions": "ignore", + "use_shallow_replication": "true", + }, + } + var c Config + _, err := c.Prepare(config, getPackerConfiguration()) + if err != nil { + t.Fatalf("expected config to accept shallow replication with unset replica count build: %v", err) + } +} func TestConfigShouldRejectManagedImageOSDiskSnapshotNameAndManagedImageDataDiskSnapshotPrefixWithCaptureContainerName(t *testing.T) { config := map[string]interface{}{ "image_offer": "ignore", diff --git a/builder/azure/arm/step_publish_to_shared_image_gallery.go b/builder/azure/arm/step_publish_to_shared_image_gallery.go index 776988b4..e8fb61d3 100644 --- a/builder/azure/arm/step_publish_to_shared_image_gallery.go +++ b/builder/azure/arm/step_publish_to_shared_image_gallery.go @@ -16,12 +16,25 @@ import ( type StepPublishToSharedImageGallery struct { client *AzureClient - publish func(ctx context.Context, subscriptionID string, sourceID string, sharedImageGallery SharedImageGalleryDestination, miSGImageVersionEndOfLifeDate string, miSGImageVersionExcludeFromLatest bool, miSigReplicaCount int64, location string, diskEncryptionSetId string, tags map[string]string) (string, error) + publish func(ctx context.Context, args PublishArgs) (string, error) say func(message string) error func(e error) toSIG func() bool } +type PublishArgs struct { + SubscriptionID string + SourceID string + SharedImageGallery SharedImageGalleryDestination + EndOfLifeDate string + ExcludeFromLatest bool + ReplicaCount int64 + Location string + DiskEncryptionSetId string + ReplicationMode galleryimageversions.ReplicationMode + Tags map[string]string +} + func NewStepPublishToSharedImageGallery(client *AzureClient, ui packersdk.Ui, config *Config) *StepPublishToSharedImageGallery { var step = &StepPublishToSharedImageGallery{ client: client, @@ -72,43 +85,45 @@ func getSigDestination(state multistep.StateBag) SharedImageGalleryDestination { } } -func (s *StepPublishToSharedImageGallery) publishToSig(ctx context.Context, subscriptionID string, sourceID string, sharedImageGallery SharedImageGalleryDestination, miSGImageVersionEndOfLifeDate string, miSGImageVersionExcludeFromLatest bool, miSigReplicaCount int64, location string, diskEncryptionSetId string, tags map[string]string) (string, error) { - replicationRegions := make([]galleryimageversions.TargetRegion, len(sharedImageGallery.SigDestinationReplicationRegions)) - for i, v := range sharedImageGallery.SigDestinationReplicationRegions { +func (s *StepPublishToSharedImageGallery) publishToSig(ctx context.Context, args PublishArgs) (string, error) { + replicationRegions := make([]galleryimageversions.TargetRegion, len(args.SharedImageGallery.SigDestinationReplicationRegions)) + for i, v := range args.SharedImageGallery.SigDestinationReplicationRegions { regionName := v replicationRegions[i] = galleryimageversions.TargetRegion{Name: regionName} } - storageAccountType, err := getSigDestinationStorageAccountType(sharedImageGallery.SigDestinationStorageAccountType) + storageAccountType, err := getSigDestinationStorageAccountType(args.SharedImageGallery.SigDestinationStorageAccountType) if err != nil { s.error(err) return "", err } - if diskEncryptionSetId != "" { + if args.DiskEncryptionSetId != "" { for index, targetRegion := range replicationRegions { targetRegion.Encryption = &galleryimageversions.EncryptionImages{ OsDiskImage: &galleryimageversions.OSDiskImageEncryption{ - DiskEncryptionSetId: &diskEncryptionSetId, + DiskEncryptionSetId: &args.DiskEncryptionSetId, }, } replicationRegions[index] = targetRegion } } + galleryImageVersion := galleryimageversions.GalleryImageVersion{ - Location: location, - Tags: &tags, + Location: args.Location, + Tags: &args.Tags, Properties: &galleryimageversions.GalleryImageVersionProperties{ StorageProfile: galleryimageversions.GalleryImageVersionStorageProfile{ Source: &galleryimageversions.GalleryArtifactVersionFullSource{ - Id: &sourceID, + Id: &args.SourceID, }, }, PublishingProfile: &galleryimageversions.GalleryArtifactPublishingProfileBase{ TargetRegions: &replicationRegions, - EndOfLifeDate: &miSGImageVersionEndOfLifeDate, - ExcludeFromLatest: &miSGImageVersionExcludeFromLatest, - ReplicaCount: &miSigReplicaCount, + EndOfLifeDate: &args.EndOfLifeDate, + ExcludeFromLatest: &args.ExcludeFromLatest, + ReplicaCount: &args.ReplicaCount, + ReplicationMode: &args.ReplicationMode, StorageAccountType: &storageAccountType, }, }, @@ -116,7 +131,7 @@ func (s *StepPublishToSharedImageGallery) publishToSig(ctx context.Context, subs pollingContext, cancel := context.WithTimeout(ctx, s.client.SharedGalleryTimeout) defer cancel() - galleryImageVersionId := galleryimageversions.NewImageVersionID(subscriptionID, sharedImageGallery.SigDestinationResourceGroup, sharedImageGallery.SigDestinationGalleryName, sharedImageGallery.SigDestinationImageName, sharedImageGallery.SigDestinationImageVersion) + galleryImageVersionId := galleryimageversions.NewImageVersionID(args.SubscriptionID, args.SharedImageGallery.SigDestinationResourceGroup, args.SharedImageGallery.SigDestinationGalleryName, args.SharedImageGallery.SigDestinationImageName, args.SharedImageGallery.SigDestinationImageVersion) err = s.client.GalleryImageVersionsClient.CreateOrUpdateThenPoll(pollingContext, galleryImageVersionId, galleryImageVersion) if err != nil { s.say(s.client.LastError.Error()) @@ -187,9 +202,28 @@ func (s *StepPublishToSharedImageGallery) Run(ctx context.Context, stateBag mult s.say(fmt.Sprintf(" -> SIG image version endoflife date : '%s'", miSGImageVersionEndOfLifeDate)) s.say(fmt.Sprintf(" -> SIG image version exclude from latest : '%t'", miSGImageVersionExcludeFromLatest)) s.say(fmt.Sprintf(" -> SIG replica count [1, 100] : '%d'", miSigReplicaCount)) - + replicationMode := galleryimageversions.ReplicationModeFull + shallowReplicationMode := stateBag.Get(constants.ArmSharedImageGalleryDestinationShallowReplication).(bool) + if shallowReplicationMode { + s.say(" -> Creating SIG Image with Shallow Replication") + replicationMode = galleryimageversions.ReplicationModeShallow + } subscriptionID := stateBag.Get(constants.ArmSharedImageGalleryDestinationSubscription).(string) - createdGalleryImageVersionID, err := s.publish(ctx, subscriptionID, sourceID, sharedImageGallery, miSGImageVersionEndOfLifeDate, miSGImageVersionExcludeFromLatest, miSigReplicaCount, location, diskEncryptionSetId, tags) + createdGalleryImageVersionID, err := s.publish( + ctx, + PublishArgs{ + SubscriptionID: subscriptionID, + SourceID: sourceID, + SharedImageGallery: sharedImageGallery, + EndOfLifeDate: miSGImageVersionEndOfLifeDate, + ExcludeFromLatest: miSGImageVersionExcludeFromLatest, + ReplicaCount: miSigReplicaCount, + Location: location, + DiskEncryptionSetId: diskEncryptionSetId, + ReplicationMode: replicationMode, + Tags: tags, + }, + ) if err != nil { stateBag.Put(constants.Error, err) diff --git a/builder/azure/arm/step_publish_to_shared_image_gallery_test.go b/builder/azure/arm/step_publish_to_shared_image_gallery_test.go index c3dbe999..7f4b5404 100644 --- a/builder/azure/arm/step_publish_to_shared_image_gallery_test.go +++ b/builder/azure/arm/step_publish_to_shared_image_gallery_test.go @@ -7,6 +7,8 @@ import ( "context" "testing" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/go-azure-sdk/resource-manager/compute/2022-03-01/images" "github.com/hashicorp/packer-plugin-azure/builder/azure/common" @@ -16,7 +18,7 @@ import ( func TestStepPublishToSharedImageGalleryShouldNotPublishForVhd(t *testing.T) { var testSubject = &StepPublishToSharedImageGallery{ - publish: func(context.Context, string, string, SharedImageGalleryDestination, string, bool, int64, string, string, map[string]string) (string, error) { + publish: func(context.Context, PublishArgs) (string, error) { return "test", nil }, say: func(message string) {}, @@ -37,7 +39,7 @@ func TestStepPublishToSharedImageGalleryShouldNotPublishForVhd(t *testing.T) { func TestStepPublishToSharedImageGalleryShouldPublishForManagedImageWithSig(t *testing.T) { var testSubject = &StepPublishToSharedImageGallery{ - publish: func(context.Context, string, string, SharedImageGalleryDestination, string, bool, int64, string, string, map[string]string) (string, error) { + publish: func(context.Context, PublishArgs) (string, error) { return "", nil }, say: func(message string) {}, @@ -57,8 +59,79 @@ func TestStepPublishToSharedImageGalleryShouldPublishForManagedImageWithSig(t *t } func TestStepPublishToSharedImageGalleryShouldPublishForNonManagedImageWithSig(t *testing.T) { + var actualPublishArgs PublishArgs + expectedPublishArgs := PublishArgs{ + SubscriptionID: "Unit Test: ManagedImageSubscription", + ReplicaCount: 1, + ReplicationMode: "Full", + Location: "Unit Test: Location", + SourceID: "Unit Test: VM ID", + SharedImageGallery: SharedImageGalleryDestination{ + SigDestinationGalleryName: "Unit Test: ManagedImageSharedGalleryName", + SigDestinationImageName: "Unit Test: ManagedImageSharedGalleryImageName", + SigDestinationSubscription: "Unit Test: ManagedImageSubscription", + SigDestinationImageVersion: "Unit Test: ManagedImageSharedGalleryImageVersion", + SigDestinationResourceGroup: "Unit Test: ManagedImageSigPublishResourceGroup", + SigDestinationReplicationRegions: []string{ + "ManagedImageSharedGalleryReplicationRegionA", + "ManagedImageSharedGalleryReplicationRegionB", + }, + SigDestinationStorageAccountType: "Standard_LRS", + }, + Tags: map[string]string{"tag01": "Unit Test: Tags"}, + } + var testSubject = &StepPublishToSharedImageGallery{ + publish: func(ctx context.Context, args PublishArgs) (string, error) { + actualPublishArgs = args + return "", nil + }, + say: func(message string) {}, + error: func(e error) {}, + toSIG: func() bool { return true }, + } + + stateBag := createTestStateBagStepPublishToSharedImageGallery(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 diff := cmp.Diff(actualPublishArgs, expectedPublishArgs, []cmp.Option{ + cmpopts.IgnoreUnexported(PublishArgs{}), + }...); diff != "" { + t.Fatalf("Unexpected diff %s", diff) + } +} + +func TestStepPublishToSharedImageGalleryShouldPublishWithShallowReplication(t *testing.T) { + var actualPublishArgs PublishArgs + expectedPublishArgs := PublishArgs{ + SubscriptionID: "Unit Test: ManagedImageSubscription", + ReplicaCount: 1, + ReplicationMode: "Shallow", + Location: "Unit Test: Location", + SourceID: "Unit Test: VM ID", + SharedImageGallery: SharedImageGalleryDestination{ + SigDestinationGalleryName: "Unit Test: ManagedImageSharedGalleryName", + SigDestinationImageName: "Unit Test: ManagedImageSharedGalleryImageName", + SigDestinationSubscription: "Unit Test: ManagedImageSubscription", + SigDestinationImageVersion: "Unit Test: ManagedImageSharedGalleryImageVersion", + SigDestinationResourceGroup: "Unit Test: ManagedImageSigPublishResourceGroup", + SigDestinationReplicationRegions: []string{ + "ManagedImageSharedGalleryReplicationRegionA", + "ManagedImageSharedGalleryReplicationRegionB", + }, + SigDestinationStorageAccountType: "Standard_LRS", + }, + Tags: map[string]string{"tag01": "Unit Test: Tags"}, + } var testSubject = &StepPublishToSharedImageGallery{ - publish: func(context.Context, string, string, SharedImageGalleryDestination, string, bool, int64, string, string, map[string]string) (string, error) { + publish: func(ctx context.Context, args PublishArgs) (string, error) { + actualPublishArgs = args return "", nil }, say: func(message string) {}, @@ -67,6 +140,7 @@ func TestStepPublishToSharedImageGalleryShouldPublishForNonManagedImageWithSig(t } stateBag := createTestStateBagStepPublishToSharedImageGallery(false) + stateBag.Put(constants.ArmSharedImageGalleryDestinationShallowReplication, true) var result = testSubject.Run(context.Background(), stateBag) if result != multistep.ActionContinue { t.Fatalf("Expected the step to return 'ActionContinue', but got '%d'.", result) @@ -75,6 +149,62 @@ func TestStepPublishToSharedImageGalleryShouldPublishForNonManagedImageWithSig(t if _, ok := stateBag.GetOk(constants.Error); ok == true { t.Fatalf("Expected the step to not set stateBag['%s'], but it was.", constants.Error) } + + if diff := cmp.Diff(actualPublishArgs, expectedPublishArgs, []cmp.Option{ + cmpopts.IgnoreUnexported(PublishArgs{}), + }...); diff != "" { + t.Fatalf("Unexpected diff %s", diff) + } +} + +func TestStepPublishToSharedImageGalleryShouldPublishWithReplicationCount(t *testing.T) { + var actualPublishArgs PublishArgs + expectedPublishArgs := PublishArgs{ + SubscriptionID: "Unit Test: ManagedImageSubscription", + ReplicaCount: 5, + ReplicationMode: "Full", + Location: "Unit Test: Location", + SourceID: "Unit Test: VM ID", + SharedImageGallery: SharedImageGalleryDestination{ + SigDestinationGalleryName: "Unit Test: ManagedImageSharedGalleryName", + SigDestinationImageName: "Unit Test: ManagedImageSharedGalleryImageName", + SigDestinationSubscription: "Unit Test: ManagedImageSubscription", + SigDestinationImageVersion: "Unit Test: ManagedImageSharedGalleryImageVersion", + SigDestinationResourceGroup: "Unit Test: ManagedImageSigPublishResourceGroup", + SigDestinationReplicationRegions: []string{ + "ManagedImageSharedGalleryReplicationRegionA", + "ManagedImageSharedGalleryReplicationRegionB", + }, + SigDestinationStorageAccountType: "Standard_LRS", + }, + Tags: map[string]string{"tag01": "Unit Test: Tags"}, + } + var testSubject = &StepPublishToSharedImageGallery{ + publish: func(ctx context.Context, args PublishArgs) (string, error) { + actualPublishArgs = args + return "", nil + }, + say: func(message string) {}, + error: func(e error) {}, + toSIG: func() bool { return true }, + } + + stateBag := createTestStateBagStepPublishToSharedImageGallery(false) + stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersionReplicaCount, int64(5)) + 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 diff := cmp.Diff(actualPublishArgs, expectedPublishArgs, []cmp.Option{ + cmpopts.IgnoreUnexported(PublishArgs{}), + }...); diff != "" { + t.Fatalf("Unexpected diff %s", diff) + } } func createTestStateBagStepPublishToSharedImageGallery(managed bool) multistep.StateBag { @@ -104,6 +234,7 @@ func createTestStateBagStepPublishToSharedImageGallery(managed bool) multistep.S stateBag.Put(constants.ArmSharedImageGalleryDestinationSubscription, "Unit Test: ManagedImageSubscription") stateBag.Put(constants.ArmIsManagedImage, managed) stateBag.Put(constants.ArmIsSIGImage, true) + stateBag.Put(constants.ArmSharedImageGalleryDestinationShallowReplication, false) return stateBag } diff --git a/builder/azure/arm/testdata/arm_linux_specialized.pkr.hcl b/builder/azure/arm/testdata/arm_linux_specialized.pkr.hcl index 3be91c29..1baffd3e 100644 --- a/builder/azure/arm/testdata/arm_linux_specialized.pkr.hcl +++ b/builder/azure/arm/testdata/arm_linux_specialized.pkr.hcl @@ -4,25 +4,26 @@ locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } variable "ssh_private_key_location" { default = "${env("ARM_SSH_PRIVATE_KEY_FILE")}" - type = string + type = string } source "azure-arm" "linux-sig" { - image_offer = "0001-com-ubuntu-server-jammy" - image_publisher = "canonical" - image_sku = "22_04-lts-arm64" - use_azure_cli_auth = true - location = "South Central US" - vm_size = "Standard_D4ps_v5" - ssh_username = "packer" + image_offer = "0001-com-ubuntu-server-jammy" + image_publisher = "canonical" + image_sku = "22_04-lts-arm64" + use_azure_cli_auth = true + location = "South Central US" + vm_size = "Standard_D4ps_v5" + ssh_username = "packer" ssh_private_key_file = var.ssh_private_key_location - communicator = "ssh" + communicator = "ssh" shared_image_gallery_destination { - image_name = "arm-linux-specialized-sig" - gallery_name = "acctestgallery" - image_version = "1.0.0" - resource_group = "packer-acceptance-test" - specialized = true + image_name = "arm-linux-specialized-sig" + gallery_name = "acctestgallery" + image_version = "1.0.0" + resource_group = "packer-acceptance-test" + specialized = true + use_shallow_replication = true } os_type = "Linux" diff --git a/builder/azure/arm/testdata/child_from_specialized_parent.pkr.hcl b/builder/azure/arm/testdata/child_from_specialized_parent.pkr.hcl index 9d8c742b..5679b0bd 100644 --- a/builder/azure/arm/testdata/child_from_specialized_parent.pkr.hcl +++ b/builder/azure/arm/testdata/child_from_specialized_parent.pkr.hcl @@ -3,24 +3,24 @@ locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") } variable "subscription" { - default = "${env("ARM_SUBSCRIPTION_ID")}" - type = string + default = "${env("ARM_SUBSCRIPTION_ID")}" + type = string sensitive = true } variable "ssh_private_key_location" { default = "${env("ARM_SSH_PRIVATE_KEY_FILE")}" - type = string + type = string } source "azure-arm" "linux-sig" { - use_azure_cli_auth = true - location = "South Central US" - vm_size = "Standard_D4ps_v5" - ssh_username = "packer" + use_azure_cli_auth = true + location = "South Central US" + vm_size = "Standard_D4ps_v5" + ssh_username = "packer" ssh_private_key_file = var.ssh_private_key_location - communicator = "ssh" - shared_image_gallery{ + communicator = "ssh" + shared_image_gallery { subscription = var.subscription image_name = "arm-linux-specialized-sig" gallery_name = "acctestgallery" @@ -28,11 +28,12 @@ source "azure-arm" "linux-sig" { resource_group = "packer-acceptance-test" } shared_image_gallery_destination { - image_name = "arm-linux-specialized-sig" - gallery_name = "acctestgallery" - image_version = "1.0.1" - resource_group = "packer-acceptance-test" - specialized = true + image_name = "arm-linux-specialized-sig" + gallery_name = "acctestgallery" + image_version = "1.0.1" + resource_group = "packer-acceptance-test" + specialized = true + use_shallow_replication = true } os_type = "Linux" diff --git a/builder/azure/common/constants/stateBag.go b/builder/azure/common/constants/stateBag.go index 2da0e55d..6a3edf55 100644 --- a/builder/azure/common/constants/stateBag.go +++ b/builder/azure/common/constants/stateBag.go @@ -56,6 +56,7 @@ const ( ArmManagedImageSharedGalleryImageVersionStorageAccountType string = "arm.ArmManagedImageSharedGalleryImageVersionStorageAccountType" ArmSharedImageGalleryDestinationSubscription string = "arm.ArmSharedImageGalleryDestinationSubscription" ArmSharedImageGalleryDestinationSpecialized string = "arm.ArmSharedImageGalleryDestinationSpecialized" + ArmSharedImageGalleryDestinationShallowReplication string = "arm.ArmSharedImageGalleryDestinationShallowReplication" ArmManagedImageSubscription string = "arm.ArmManagedImageSubscription" ArmAsyncResourceGroupDelete string = "arm.AsyncResourceGroupDelete" ArmManagedImageOSDiskSnapshotName string = "arm.ManagedImageOSDiskSnapshotName" diff --git a/docs-partials/builder/azure/arm/Config-not-required.mdx b/docs-partials/builder/azure/arm/Config-not-required.mdx index 8ecff616..62d3806f 100644 --- a/docs-partials/builder/azure/arm/Config-not-required.mdx +++ b/docs-partials/builder/azure/arm/Config-not-required.mdx @@ -86,9 +86,9 @@ - `shared_gallery_image_version_end_of_life_date` (string) - The end of life date (2006-01-02T15:04:05.99Z) of the gallery Image Version. This property can be used for decommissioning purposes. -- `shared_image_gallery_replica_count` (int32) - The number of replicas of the Image Version to be created per region. This - property would take effect for a region when regionalReplicaCount is not specified. +- `shared_image_gallery_replica_count` (int64) - The number of replicas of the Image Version to be created per region. Replica count must be between 1 and 100, but 50 replicas should be sufficient for most use cases. + When using shallow replication `use_shallow_replication=true` the value can only be 1. - `shared_gallery_image_version_exclude_from_latest` (bool) - If set to true, Virtual Machines deployed from the latest version of the Image Definition won't use this Image Version. diff --git a/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx b/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx index b20654c9..a6abe964 100644 --- a/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx +++ b/docs-partials/builder/azure/arm/SharedImageGalleryDestination-not-required.mdx @@ -10,11 +10,15 @@ - `image_version` (string) - Sig Destination Image Version -- `replication_regions` ([]string) - Sig Destination Replication Regions +- `replication_regions` ([]string) - A list of regions to replicate the image version in, by default the build location will be used as a replication region (the build location is either set in the location field, or the location of the resource group used in `build_resource_group_name` will be included. + Can not contain any region but the build region when using shallow replication - `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 build VM's OS state as Generalized +- `use_shallow_replication` (bool) - Setting a `shared_image_gallery_replica_count` or any `replication_regions` is unnecessary for shallow builds, as they can only replicate to the build region and must have a replica count of 1 + Refer to [Shallow Replication](https://learn.microsoft.com/en-us/azure/virtual-machines/shared-image-galleries?tabs=azure-cli#shallow-replication) for details on when to use shallow replication mode. +