// +build integration package azuretest import ( "context" "errors" "fmt" "io/ioutil" "log" "net/url" "os" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" "github.com/Azure/azure-storage-blob-go/azblob" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/osbuild/osbuild-composer/internal/upload/azure" ) // wrapErrorf returns error constructed using fmt.Errorf from format and any // other args. If innerError != nil, it's appended at the end of the new // error. func wrapErrorf(innerError error, format string, a ...interface{}) error { if innerError != nil { a = append(a, innerError) return fmt.Errorf(format+"\n\ninner error: %#s", a...) } return fmt.Errorf(format, a...) } type azureCredentials struct { azure.Credentials ContainerName string SubscriptionID string ClientID string ClientSecret string TenantID string Location string ResourceGroup string } // getAzureCredentialsFromEnv gets the credentials from environment variables // If none of the environment variables is set, it returns nil. // If some but not all environment variables are set, it returns an error. func GetAzureCredentialsFromEnv() (*azureCredentials, error) { storageAccount, saExists := os.LookupEnv("AZURE_STORAGE_ACCOUNT") storageAccessKey, sakExists := os.LookupEnv("AZURE_STORAGE_ACCESS_KEY") containerName, cExists := os.LookupEnv("AZURE_CONTAINER_NAME") subscriptionId, siExists := os.LookupEnv("AZURE_SUBSCRIPTION_ID") clientId, ciExists := os.LookupEnv("AZURE_CLIENT_ID") clientSecret, csExists := os.LookupEnv("AZURE_CLIENT_SECRET") tenantId, tiExists := os.LookupEnv("AZURE_TENANT_ID") location, lExists := os.LookupEnv("AZURE_LOCATION") resourceGroup, rgExists := os.LookupEnv("AZURE_RESOURCE_GROUP") // If non of the variables is set, just ignore the test if !saExists && !sakExists && !cExists && !siExists && !ciExists && !csExists && !tiExists && !lExists && !rgExists { return nil, nil } // If only one/two of them are not set, then fail if !saExists || !sakExists || !cExists || !siExists || !ciExists || !csExists || !tiExists || !lExists || !rgExists { return nil, errors.New("not all required env variables were set") } return &azureCredentials{ Credentials: azure.Credentials{ StorageAccount: storageAccount, StorageAccessKey: storageAccessKey, }, ContainerName: containerName, SubscriptionID: subscriptionId, ClientID: clientId, ClientSecret: clientSecret, TenantID: tenantId, Location: location, ResourceGroup: resourceGroup, }, nil } // UploadImageToAzure mimics the upload feature of osbuild-composer. func UploadImageToAzure(c *azureCredentials, imagePath string, imageName string) error { metadata := azure.ImageMetadata{ ContainerName: c.ContainerName, ImageName: imageName, } err := azure.UploadImage(c.Credentials, metadata, imagePath, 16) if err != nil { return fmt.Errorf("upload to azure failed: %v", err) } return nil } // DeleteImageFromAzure deletes the image uploaded by osbuild-composer // (or UpluadImageToAzure method). func DeleteImageFromAzure(c *azureCredentials, imageName string) error { // Create a default request pipeline using your storage account name and account key. credential, err := azblob.NewSharedKeyCredential(c.StorageAccount, c.StorageAccessKey) if err != nil { return err } p := azblob.NewPipeline(credential, azblob.PipelineOptions{}) // get storage account blob service URL endpoint. URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.StorageAccount, c.ContainerName)) // Create a ContainerURL object that wraps the container URL and a request // pipeline to make requests. containerURL := azblob.NewContainerURL(*URL, p) // Create the container, use a never-expiring context ctx := context.Background() blobURL := containerURL.NewPageBlobURL(imageName) _, err = blobURL.Delete(ctx, azblob.DeleteSnapshotsOptionInclude, azblob.BlobAccessConditions{}) if err != nil { return fmt.Errorf("cannot delete the image: %v", err) } return nil } // readPublicKey reads the public key from a file and returns it as a string func readPublicKey(publicKeyFile string) (string, error) { publicKey, err := ioutil.ReadFile(publicKeyFile) if err != nil { return "", fmt.Errorf("cannot read the public key file: %v", err) } return string(publicKey), nil } // deleteResource is a convenient wrapper around Azure SDK to delete a resource func deleteResource(client resources.Client, id string, apiVersion string) error { deleteFuture, err := client.DeleteByID(context.Background(), id, apiVersion) if err != nil { return fmt.Errorf("cannot delete the resourceType %s: %v", id, err) } err = deleteFuture.WaitForCompletionRef(context.Background(), client.BaseClient.Client) if err != nil { return fmt.Errorf("waiting for the resourceType %s deletion failed: %v", id, err) } _, err = deleteFuture.Result(client) if err != nil { return fmt.Errorf("cannot retrieve the result of %s deletion: %v", id, err) } return nil } func NewDeploymentParameters(creds *azureCredentials, imageName, testId, publicKey string) DeploymentParameters { // Azure requires a lot of names - for a virtual machine, a virtual network, // a virtual interface and so on and so forth. // Let's create all of them here from the test id so we can delete them // later. imagePath := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", creds.StorageAccount, creds.ContainerName, imageName) return DeploymentParameters{ NetworkInterfaceName: newDeploymentParameter("iface-" + testId), NetworkSecurityGroupName: newDeploymentParameter("nsg-" + testId), VirtualNetworkName: newDeploymentParameter("vnet-" + testId), PublicIPAddressName: newDeploymentParameter("ip-" + testId), VirtualMachineName: newDeploymentParameter("vm-" + testId), DiskName: newDeploymentParameter("disk-" + testId), ImageName: newDeploymentParameter("image-" + testId), Location: newDeploymentParameter(creds.Location), ImagePath: newDeploymentParameter(imagePath), AdminUsername: newDeploymentParameter("redhat"), AdminPublicKey: newDeploymentParameter(publicKey), } } func CleanUpBootedVM(creds *azureCredentials, parameters DeploymentParameters, authorizer autorest.Authorizer, testId string) (retErr error) { deploymentName := testId deploymentsClient := resources.NewDeploymentsClient(creds.SubscriptionID) deploymentsClient.Authorizer = authorizer resourcesClient := resources.NewClient(creds.SubscriptionID) resourcesClient.Authorizer = authorizer // This array specifies all the resources we need to delete. The // order is important, e.g. one cannot delete a network interface // that is still attached to a virtual machine. resourcesToDelete := []struct { resType string name string apiVersion string }{ { resType: "Microsoft.Compute/virtualMachines", name: parameters.VirtualMachineName.Value, apiVersion: "2019-07-01", }, { resType: "Microsoft.Network/networkInterfaces", name: parameters.NetworkInterfaceName.Value, apiVersion: "2019-09-01", }, { resType: "Microsoft.Network/publicIPAddresses", name: parameters.PublicIPAddressName.Value, apiVersion: "2019-09-01", }, { resType: "Microsoft.Network/networkSecurityGroups", name: parameters.NetworkSecurityGroupName.Value, apiVersion: "2019-09-01", }, { resType: "Microsoft.Network/virtualNetworks", name: parameters.VirtualNetworkName.Value, apiVersion: "2019-09-01", }, { resType: "Microsoft.Compute/disks", name: parameters.DiskName.Value, apiVersion: "2019-07-01", }, { resType: "Microsoft.Compute/images", name: parameters.ImageName.Value, apiVersion: "2019-07-01", }, } // Delete all the resources for _, resourceToDelete := range resourcesToDelete { resourceID := fmt.Sprintf( "subscriptions/%s/resourceGroups/%s/providers/%s/%s", creds.SubscriptionID, creds.ResourceGroup, resourceToDelete.resType, resourceToDelete.name, ) err := deleteResource(resourcesClient, resourceID, resourceToDelete.apiVersion) if err != nil { log.Printf("deleting the resource %s errored: %v", resourceToDelete.name, err) retErr = wrapErrorf(retErr, "cannot delete the resource %s: %v", resourceToDelete.name, err) // do not return here, try deleting as much as possible } } // Delete the deployment // This actually does not delete any resources created by the // deployment as one might think. Therefore the code above // is needed. result, err := deploymentsClient.Delete(context.Background(), creds.ResourceGroup, deploymentName) if err != nil { retErr = wrapErrorf(retErr, "cannot create the request for the deployment deletion: %v", err) return } err = result.WaitForCompletionRef(context.Background(), deploymentsClient.Client) if err != nil { retErr = wrapErrorf(retErr, "waiting for the deployment deletion failed: %v", err) return } _, err = result.Result(deploymentsClient) if err != nil { retErr = wrapErrorf(retErr, "cannot retrieve the deployment deletion result: %v", err) return } return } // WithBootedImageInAzure runs the function f in the context of booted // image in Azure func WithBootedImageInAzure(creds *azureCredentials, imageName, testId, publicKeyFile string, f func(address string) error) (retErr error) { publicKey, err := readPublicKey(publicKeyFile) if err != nil { return err } clientCredentialsConfig := auth.NewClientCredentialsConfig(creds.ClientID, creds.ClientSecret, creds.TenantID) authorizer, err := clientCredentialsConfig.Authorizer() if err != nil { return fmt.Errorf("cannot create the authorizer: %v", err) } template, err := loadDeploymentTemplate() if err != nil { return err } deploymentsClient := resources.NewDeploymentsClient(creds.SubscriptionID) deploymentsClient.Authorizer = authorizer deploymentName := testId parameters := NewDeploymentParameters(creds, imageName, testId, publicKey) deploymentFuture, err := deploymentsClient.CreateOrUpdate(context.Background(), creds.ResourceGroup, deploymentName, resources.Deployment{ Properties: &resources.DeploymentProperties{ Mode: resources.Incremental, Template: template, Parameters: parameters, }, }) // Let's registed the clean-up function as soon as possible. defer func() { retErr = CleanUpBootedVM(creds, parameters, authorizer, testId) }() if err != nil { return fmt.Errorf("creating a deployment failed: %v", err) } err = deploymentFuture.WaitForCompletionRef(context.Background(), deploymentsClient.Client) if err != nil { return fmt.Errorf("waiting for deployment completion failed: %v", err) } _, err = deploymentFuture.Result(deploymentsClient) if err != nil { return fmt.Errorf("retrieving the deployment result failed: %v", err) } // get the IP address publicIPAddressClient := network.NewPublicIPAddressesClient(creds.SubscriptionID) publicIPAddressClient.Authorizer = authorizer publicIPAddress, err := publicIPAddressClient.Get(context.Background(), creds.ResourceGroup, parameters.PublicIPAddressName.Value, "") if err != nil { return fmt.Errorf("cannot get the ip address details: %v", err) } return f(*publicIPAddress.IPAddress) }