Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource: azurerm_container_registry_credential_set #27528

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,4 @@ service/vmware:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(vmware_cluster\W+|vmware_express_route_authorization\W+|vmware_netapp_volume_attachment\W+|vmware_private_cloud\W+|voice_services_communications_gateway\W+|voice_services_communications_gateway_test_line\W+)((.|\n)*)###'

service/workloads:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(chaos_studio_|container_connected_registry\W+|container_registry_cache_rule\W+|container_registry_cache_rule\W+|container_registry_task\W+|container_registry_task_schedule_run_now\W+|container_registry_token_password\W+|kubernetes_cluster_extension\W+|kubernetes_cluster_trusted_access_role_binding\W+|kubernetes_fleet_manager\W+|kubernetes_fleet_manager\W+|kubernetes_fleet_member\W+|kubernetes_fleet_update_run\W+|kubernetes_fleet_update_strategy\W+|kubernetes_flux_configuration\W+|kubernetes_node_pool_snapshot\W+|workloads_sap_)((.|\n)*)###'
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(chaos_studio_|container_connected_registry\W+|container_registry_cache_rule\W+|container_registry_cache_rule\W+|container_registry_credential_set\W+|container_registry_task\W+|container_registry_task_schedule_run_now\W+|container_registry_token_password\W+|kubernetes_cluster_extension\W+|kubernetes_cluster_trusted_access_role_binding\W+|kubernetes_fleet_manager\W+|kubernetes_fleet_manager\W+|kubernetes_fleet_member\W+|kubernetes_fleet_update_run\W+|kubernetes_fleet_update_strategy\W+|kubernetes_flux_configuration\W+|kubernetes_node_pool_snapshot\W+|workloads_sap_)((.|\n)*)###'
9 changes: 9 additions & 0 deletions internal/services/containers/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/go-azure-sdk/resource-manager/containerinstance/2023-05-01/containerinstance"
containerregistry_v2019_06_01_preview "github.com/hashicorp/go-azure-sdk/resource-manager/containerregistry/2019-06-01-preview"
"github.com/hashicorp/go-azure-sdk/resource-manager/containerregistry/2023-07-01/cacherules"
"github.com/hashicorp/go-azure-sdk/resource-manager/containerregistry/2023-07-01/credentialsets"
containerregistry "github.com/hashicorp/go-azure-sdk/resource-manager/containerregistry/2023-11-01-preview"
"github.com/hashicorp/go-azure-sdk/resource-manager/containerservice/2019-08-01/containerservices"
"github.com/hashicorp/go-azure-sdk/resource-manager/containerservice/2024-04-01/fleetupdatestrategies"
Expand All @@ -28,6 +29,7 @@ type Client struct {
AgentPoolsClient *agentpools.AgentPoolsClient
ContainerInstanceClient *containerinstance.ContainerInstanceClient
CacheRulesClient *cacherules.CacheRulesClient
CredentialSetsClient *credentialsets.CredentialSetsClient
ContainerRegistryClient *containerregistry.Client
// v2019_06_01_preview is needed for container registry agent pools and tasks
ContainerRegistryClient_v2019_06_01_preview *containerregistry_v2019_06_01_preview.Client
Expand Down Expand Up @@ -69,6 +71,12 @@ func NewContainersClient(o *common.ClientOptions) (*Client, error) {
}
o.Configure(cacheRulesClient.Client, o.Authorizers.ResourceManager)

credentialSetsClient, err := credentialsets.NewCredentialSetsClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Credential Sets client: %+v", err)
}
o.Configure(credentialSetsClient.Client, o.Authorizers.ResourceManager)

// AKS
fleetUpdateRunsClient, err := updateruns.NewUpdateRunsClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
Expand Down Expand Up @@ -128,6 +136,7 @@ func NewContainersClient(o *common.ClientOptions) (*Client, error) {
AgentPoolsClient: agentPoolsClient,
ContainerInstanceClient: containerInstanceClient,
CacheRulesClient: cacheRulesClient,
CredentialSetsClient: credentialSetsClient,
ContainerRegistryClient: containerRegistryClient,
ContainerRegistryClient_v2019_06_01_preview: containerRegistryClient_v2019_06_01_preview,
FleetUpdateRunsClient: fleetUpdateRunsClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
package containers

import (
"context"
"fmt"
"log"
"time"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema"
"github.com/hashicorp/go-azure-helpers/resourcemanager/identity"
"github.com/hashicorp/go-azure-sdk/resource-manager/containerregistry/2023-07-01/credentialsets"
"github.com/hashicorp/go-azure-sdk/resource-manager/containerregistry/2023-11-01-preview/registries"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)

var _ sdk.Resource = ContainerRegistryCredentialSetResource{}

type ContainerRegistryCredentialSetResource struct{}

func (ContainerRegistryCredentialSetResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"name": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
Description: "The name of the credential set.",
},
"container_registry_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: registries.ValidateRegistryID,
},
"login_server": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
},
"authentication_credentials": {
Type: pluginsdk.TypeList,
Required: true,
MaxItems: 1,
Elem: &pluginsdk.Resource{
Schema: map[string]*schema.Schema{
"username_secret_id": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: keyVaultValidate.VersionlessNestedItemId,
},
"password_secret_id": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: keyVaultValidate.VersionlessNestedItemId,
},
},
},
},
// Note [1]: At point in time of the implementation of this resource the API only accept SystemAssigned even though API Spec defines all three identity modes are possible
// Error when trying with type UserAssigned:
// code: "CannotSetResourceIdentity"
// message: "The resource identity 'UserAssigned' cannot be set on the resource of type 'Microsoft.ContainerRegistry/registries/credentialSets'."
// or with type empty:
// code: "OnlySystemManagedIdentityAllowed"
// message: "Only System Managed Identities are allowed for resources of type 'Microsoft.ContainerRegistry/registries/credentialSets'. For more information, please visit https://aka.ms/acr/cache."
"identity": commonschema.SystemAssignedIdentityRequired(),
}
}

func (ContainerRegistryCredentialSetResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}

type AuthenticationCredential struct {
UsernameSecretId string `tfschema:"username_secret_id"`
PasswordSecretId string `tfschema:"password_secret_id"`
}

type ContainerRegistryCredentialSetModel struct {
Name string `tfschema:"name"`
ContainerRegistryId string `tfschema:"container_registry_id"`
LoginServer string `tfschema:"login_server"`
AuthenticationCredential []AuthenticationCredential `tfschema:"authentication_credentials"`
Identity []identity.ModelSystemAssigned `tfschema:"identity"`
}

func (ContainerRegistryCredentialSetResource) ModelObject() interface{} {
return &ContainerRegistryCredentialSetModel{}
}

func (ContainerRegistryCredentialSetResource) ResourceType() string {
return "azurerm_container_registry_credential_set"
}

func (r ContainerRegistryCredentialSetResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Containers.CredentialSetsClient
subscriptionId := metadata.Client.Account.SubscriptionId

var config ContainerRegistryCredentialSetModel
if err := metadata.Decode(&config); err != nil {
return err
}

log.Printf("[INFO] preparing arguments for Container Registry Credential Set creation.")

registryId, err := registries.ParseRegistryID(config.ContainerRegistryId)
if err != nil {
return err
}

id := credentialsets.NewCredentialSetID(subscriptionId,
registryId.ResourceGroupName,
registryId.RegistryName,
config.Name,
)

existing, err := client.Get(ctx, id)
if err != nil && !response.WasNotFound(existing.HttpResponse) {
return fmt.Errorf("checking for presence of existing %s: %+v", id, err)
}
if !response.WasNotFound(existing.HttpResponse) {
return metadata.ResourceRequiresImport(r.ResourceType(), id)
}

param := credentialsets.CredentialSet{
Name: pointer.To(id.CredentialSetName),
Properties: &credentialsets.CredentialSetProperties{
LoginServer: pointer.To(config.LoginServer),
AuthCredentials: expandAuthCredentials(config.AuthenticationCredential),
},
Identity: expandIdentity(config.Identity),
}

if err := client.CreateThenPoll(ctx, id, param); err != nil {
return fmt.Errorf("creating %s: %+v", id, err)
}

metadata.SetID(id)
return nil
},
}
}

func (r ContainerRegistryCredentialSetResource) Update() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Containers.CredentialSetsClient
id, err := credentialsets.ParseCredentialSetID(metadata.ResourceData.Id())
if err != nil {
return err
}

param := credentialsets.CredentialSetUpdateParameters{}

var model ContainerRegistryCredentialSetModel
if err := metadata.Decode(&model); err != nil {
return err
}

properties := credentialsets.CredentialSetUpdateProperties{}

if metadata.ResourceData.HasChange("authentication_credentials") {
properties.AuthCredentials = expandAuthCredentials(model.AuthenticationCredential)
}

param.Properties = &properties

if metadata.ResourceData.HasChange("identity") {
param.Identity = expandIdentity(model.Identity)
}

if err := client.UpdateThenPoll(ctx, *id, param); err != nil {
return fmt.Errorf("updating %s: %+v", id, err)
}
return nil
},
}
}

func (ContainerRegistryCredentialSetResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Containers.CredentialSetsClient
id, err := credentialsets.ParseCredentialSetID(metadata.ResourceData.Id())
if err != nil {
return err
}

resp, err := client.Get(ctx, *id)
if err != nil {
if response.WasNotFound(resp.HttpResponse) {
return metadata.MarkAsGone(id)
}
return fmt.Errorf("retrieving %s: %+v", id, err)
}

registryId := registries.NewRegistryID(id.SubscriptionId, id.ResourceGroupName, id.RegistryName)

var config ContainerRegistryCredentialSetModel
if err := metadata.Decode(&config); err != nil {
return err
}

config.Name = id.CredentialSetName
config.ContainerRegistryId = registryId.ID()

if model := resp.Model; model != nil {
config.Identity = flattenIdentity(model.Identity)
jan-mrm marked this conversation as resolved.
Show resolved Hide resolved
if props := model.Properties; props != nil {
config.LoginServer = pointer.From(props.LoginServer)
config.AuthenticationCredential = flattenAuthCredentials(props.AuthCredentials)
}
}
return metadata.Encode(&config)
},
}
}

func (ContainerRegistryCredentialSetResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Containers.CredentialSetsClient
id, err := credentialsets.ParseCredentialSetID(metadata.ResourceData.Id())
if err != nil {
return err
}

if err := client.DeleteThenPoll(ctx, *id); err != nil {
return fmt.Errorf("deleting %s: %+v", *id, err)
}
return nil
},
}
}

func (ContainerRegistryCredentialSetResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return credentialsets.ValidateCredentialSetID
}

func expandAuthCredentials(input []AuthenticationCredential) *[]credentialsets.AuthCredential {
output := make([]credentialsets.AuthCredential, 0)
if len(input) == 0 {
return &output
}
for _, v := range input {
output = append(output, credentialsets.AuthCredential{
Name: pointer.To(credentialsets.CredentialNameCredentialOne),
UsernameSecretIdentifier: pointer.To(v.UsernameSecretId),
PasswordSecretIdentifier: pointer.To(v.PasswordSecretId),
})
}
return &output
}

func flattenAuthCredentials(input *[]credentialsets.AuthCredential) []AuthenticationCredential {
output := make([]AuthenticationCredential, 0)
if input == nil {
return output
}
for _, v := range *input {
output = append(output, AuthenticationCredential{
UsernameSecretId: pointer.From(v.UsernameSecretIdentifier),
PasswordSecretId: pointer.From(v.PasswordSecretIdentifier),
})
}
return output
}

// read the note [1] above why we transform the identity here like that
func flattenIdentity(input *identity.SystemAndUserAssignedMap) []identity.ModelSystemAssigned {
if input == nil {
return nil
}
// the api returns 'systemAssigned' as the type instead of 'SystemAssigned'...
// in the identity package a private function to normalize the type is used (normalizeType)
systemAssignedIdentity := &identity.SystemAssigned{
Type: input.Type,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call a normalising function on the identity type returned here in the flatten functions of the identity package because the API is often inconsistent with the casing

https://github.com/hashicorp/go-azure-helpers/blob/624ef4f92d9f94a4a467b2bf5fb79974ef6c3255/resourcemanager/identity/constants.go#L20-L38

We can do this and it should take care of the casing issue for us

Suggested change
Type: input.Type,
Type: identity.Type(input.Type),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested that and it did not resolve the issue 🫤

TenantId: input.TenantId,
PrincipalId: input.PrincipalId,
}
output := identity.FlattenSystemAssignedToModel(systemAssignedIdentity)
return output
}

// read the note [1] above why we transform the identity here like that
func expandIdentity(input []identity.ModelSystemAssigned) *identity.SystemAndUserAssignedMap {
if len(input) == 0 {
return nil
}
output := identity.SystemAndUserAssignedMap{
Type: input[0].Type,
TenantId: input[0].TenantId,
PrincipalId: input[0].PrincipalId,
}
return &output
}
Loading
Loading