tooling/templatize/pkg/pipeline/arm.go (279 lines of code) (raw):

// Copyright 2025 Microsoft Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package pipeline import ( "context" "fmt" "strings" "time" "github.com/Azure/ARO-Tools/pkg/config" "github.com/Azure/ARO-HCP/tooling/templatize/pkg/azauth" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources" "github.com/go-logr/logr" ) type armClient struct { deploymentClient *armresources.DeploymentsClient resourceGroupClient *armresources.ResourceGroupsClient deploymentRetryWaitTime int Region string GetDeployment func(ctx context.Context, rgName, deploymentName string) (armresources.DeploymentsClientGetResponse, error) } func newArmClient(subscriptionID, region string) *armClient { cred, err := azauth.GetAzureTokenCredentials() if err != nil { return nil } deploymentClient, err := armresources.NewDeploymentsClient(subscriptionID, cred, nil) if err != nil { return nil } resourceGroupClient, err := armresources.NewResourceGroupsClient(subscriptionID, cred, nil) if err != nil { return nil } return &armClient{ deploymentClient: deploymentClient, deploymentRetryWaitTime: 15, resourceGroupClient: resourceGroupClient, Region: region, GetDeployment: func(ctx context.Context, rgName, deploymentName string) (armresources.DeploymentsClientGetResponse, error) { return deploymentClient.Get(ctx, rgName, deploymentName, nil) }, } } func (a *armClient) getExistingDeployment(ctx context.Context, rgName, deploymentName string) (*armresources.DeploymentsClientGetResponse, error) { resp, err := a.GetDeployment(ctx, rgName, deploymentName) if err != nil && !strings.Contains(err.Error(), "ERROR CODE: DeploymentNotFound") { return nil, err } return &resp, nil } func (a *armClient) waitForExistingDeployment(ctx context.Context, timeOutInSeconds int, rgName, deploymentName string) error { for timeOutInSeconds > 0 { resp, err := a.getExistingDeployment(ctx, rgName, deploymentName) if err != nil { return fmt.Errorf("error getting deployment %w", err) } if resp.Properties == nil { return nil } if *resp.Properties.ProvisioningState != armresources.ProvisioningStateRunning { return nil } time.Sleep(time.Duration(a.deploymentRetryWaitTime) * time.Second) timeOutInSeconds -= a.deploymentRetryWaitTime } return fmt.Errorf("timeout exeeded waiting for deployment %s in rg %s", deploymentName, rgName) } func (a *armClient) runArmStep(ctx context.Context, options *PipelineRunOptions, rgName string, step *ARMStep, input map[string]output) (output, error) { // Ensure resourcegroup exists err := a.ensureResourceGroupExists(ctx, rgName, options.NoPersist) if err != nil { return nil, fmt.Errorf("failed to ensure resource group exists: %w", err) } // Run deployment if err := a.waitForExistingDeployment(ctx, options.DeploymentTimeoutSeconds, rgName, step.Name); err != nil { return nil, fmt.Errorf("error waiting for deploymenty %w", err) } if !options.DryRun || (options.DryRun && step.OutputOnly) { return doWaitForDeployment(ctx, a.deploymentClient, rgName, step, options.Configuration, input) } return doDryRun(ctx, a.deploymentClient, rgName, step, options.Configuration, input) } func recursivePrint(level int, change *armresources.WhatIfPropertyChange) { fmt.Printf("%s%s:\n", strings.Repeat("\t", level), *change.Path) fmt.Printf("%s\tBefore:%s\n", strings.Repeat("\t", level), change.Before) fmt.Printf("%s\tAfter:%s\n", strings.Repeat("\t", level), change.After) for _, child := range change.Children { level += level recursivePrint(level, child) } } func printChanges(t armresources.ChangeType, changes []*armresources.WhatIfChange) { for _, change := range changes { if *change.ChangeType == t { fmt.Printf("%s %s\n", strings.Repeat("\t", 1), *change.ResourceID) for _, delta := range change.Delta { recursivePrint(2, delta) } } } } func printChangeReport(changes []*armresources.WhatIfChange) { fmt.Println("Change report for WhatIf deployment") fmt.Println("----------") fmt.Println("Creating") printChanges(armresources.ChangeTypeCreate, changes) fmt.Println("----------") fmt.Println("Deploy") printChanges(armresources.ChangeTypeDeploy, changes) fmt.Println("----------") fmt.Println("Modify") printChanges(armresources.ChangeTypeModify, changes) fmt.Println("----------") fmt.Println("Delete") printChanges(armresources.ChangeTypeDelete, changes) fmt.Println("----------") fmt.Println("Ignoring") printChanges(armresources.ChangeTypeIgnore, changes) fmt.Println("----------") fmt.Println("NoChange") printChanges(armresources.ChangeTypeNoChange, changes) fmt.Println("----------") fmt.Println("Unsupported") printChanges(armresources.ChangeTypeUnsupported, changes) } func createError(errors armresources.ErrorResponse) error { errB, err := errors.MarshalJSON() if err != nil { return err } return fmt.Errorf("%s", string(errB)) } func pollAndPrint[T any](ctx context.Context, p *runtime.Poller[T]) error { resp, err := p.PollUntilDone(ctx, nil) if err != nil { return fmt.Errorf("failed to wait for deployment completion: %w", err) } switch m := any(resp).(type) { case armresources.DeploymentsClientWhatIfResponse: if *m.Status == "Failed" { return createError(*m.Error) } printChangeReport(m.Properties.Changes) case armresources.DeploymentsClientWhatIfAtSubscriptionScopeResponse: if *m.Status == "Failed" { return createError(*m.Error) } printChangeReport(m.Properties.Changes) default: return fmt.Errorf("unknown type %T", m) } return nil } func doDryRun(ctx context.Context, client *armresources.DeploymentsClient, rgName string, step *ARMStep, cfg config.Configuration, input map[string]output) (output, error) { logger := logr.FromContextOrDiscard(ctx) inputValues, err := getInputValues(step.Variables, cfg, input) if err != nil { return nil, fmt.Errorf("failed to get input values: %w", err) } // Transform Bicep to ARM deploymentProperties, err := transformBicepToARMWhatIfDeployment(ctx, step.Parameters, cfg, inputValues) if err != nil { return nil, fmt.Errorf("failed to transform Bicep to ARM: %w", err) } // Create the deployment deployment := armresources.DeploymentWhatIf{ Properties: deploymentProperties, } if step.DeploymentLevel == "Subscription" { // Hardcode until schema is adapted deployment.Location = to.Ptr("eastus2") poller, err := client.BeginWhatIfAtSubscriptionScope(ctx, step.Name, deployment, nil) if err != nil { return nil, fmt.Errorf("failed to create WhatIf Deployment: %w", err) } logger.Info("WhatIf Deployment started", "deployment", step.Name) err = pollAndPrint(ctx, poller) if err != nil { return nil, fmt.Errorf("failed to poll and print: %w", err) } } else { poller, err := client.BeginWhatIf(ctx, rgName, step.Name, deployment, nil) if err != nil { return nil, fmt.Errorf("failed to create WhatIf Deployment: %w", err) } logger.Info("WhatIf Deployment started", "deployment", step.Name) err = pollAndPrint(ctx, poller) if err != nil { return nil, fmt.Errorf("failed to poll and print: %w", err) } } return nil, nil } func pollAndGetOutput[T any](ctx context.Context, p *runtime.Poller[T]) (armOutput, error) { respRaw, err := p.PollUntilDone(ctx, nil) if err != nil { return nil, fmt.Errorf("failed to wait for deployment completion: %w", err) } var outputs any switch resp := any(respRaw).(type) { case armresources.DeploymentsClientCreateOrUpdateResponse: outputs = resp.Properties.Outputs case armresources.DeploymentsClientCreateOrUpdateAtSubscriptionScopeResponse: outputs = resp.Properties.Outputs default: return nil, fmt.Errorf("unknown type %T", resp) } if outputs != nil { if outputMap, ok := outputs.(map[string]any); ok { returnMap := armOutput{} for k, v := range outputMap { returnMap[k] = v } return returnMap, nil } } return nil, nil } func doWaitForDeployment(ctx context.Context, client *armresources.DeploymentsClient, rgName string, step *ARMStep, cfg config.Configuration, input map[string]output) (output, error) { logger := logr.FromContextOrDiscard(ctx) inputValues, err := getInputValues(step.Variables, cfg, input) if err != nil { return nil, fmt.Errorf("failed to get input values: %w", err) } // Transform Bicep to ARM deploymentProperties, err := transformBicepToARMDeployment(ctx, step.Parameters, cfg, inputValues) if err != nil { return nil, fmt.Errorf("failed to transform Bicep to ARM: %w", err) } if hasTemplateResources(deploymentProperties.Template) && step.OutputOnly { return nil, fmt.Errorf("deployment step %s is outputOnly, but contains resources", step.Name) } // Create the deployment deployment := armresources.Deployment{ Properties: deploymentProperties, } if step.DeploymentLevel == "Subscription" { // Hardcode until schema is adapted deployment.Location = to.Ptr("eastus2") poller, err := client.BeginCreateOrUpdateAtSubscriptionScope(ctx, step.Name, deployment, nil) if err != nil { return nil, fmt.Errorf("failed to create deployment: %w", err) } logger.V(1).Info("Deployment started", "deployment", step.Name) return pollAndGetOutput(ctx, poller) } else { poller, err := client.BeginCreateOrUpdate(ctx, rgName, step.Name, deployment, nil) if err != nil { return nil, fmt.Errorf("failed to create deployment: %w", err) } logger.V(1).Info("Deployment started", "deployment", step.Name) return pollAndGetOutput(ctx, poller) } } func (a *armClient) ensureResourceGroupExists(ctx context.Context, rgName string, rgNoPersist bool) error { // Check if the resource group exists // Once the persist tag is set to true, it should not be removed by automation... tooo dangerous rg, err := a.resourceGroupClient.Get(ctx, rgName, nil) if err != nil { // Create the resource group tags := map[string]*string{} if !rgNoPersist { // if no-persist is set, don't set the persist tag, needs double negotiate, cause default should be true tags["persist"] = to.Ptr("true") } resourceGroup := armresources.ResourceGroup{ Location: to.Ptr(a.Region), Tags: tags, } _, err = a.resourceGroupClient.CreateOrUpdate(ctx, rgName, resourceGroup, nil) if err != nil { return fmt.Errorf("failed to create resource group: %w", err) } } else { tags := rg.Tags if tags == nil { tags = map[string]*string{} } if !rgNoPersist { // if no-persist is set, don't set the persist tag, needs double negotiate, cause default should be true tags["persist"] = to.Ptr("true") } patchResourceGroup := armresources.ResourceGroupPatchable{ Tags: tags, } _, err = a.resourceGroupClient.Update(ctx, rgName, patchResourceGroup, nil) if err != nil { return fmt.Errorf("failed to update resource group: %w", err) } } return nil }