From c4391eafc77105eae0ec22999de9d03eb097cc92 Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Thu, 15 Aug 2024 13:02:33 -0400 Subject: [PATCH] url: add azure blob fetching support for ignition files use azure sdk to authorize, initiate and fetch ignition config file from azure blob storage. fixes: https://issues.redhat.com/browse/COS-2859 --- docs/release-notes.md | 2 ++ internal/resource/url.go | 66 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 93082d68f..5de191ab9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,8 @@ Starting with this release, ignition-validate binaries are signed with the ### Features +- Add Azure blob support for fetching ignition configs + ### Changes ### Bug fixes diff --git a/internal/resource/url.go b/internal/resource/url.go index 9262b5dda..80c0022b3 100644 --- a/internal/resource/url.go +++ b/internal/resource/url.go @@ -39,6 +39,8 @@ import ( "golang.org/x/oauth2/google" "google.golang.org/api/option" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/awserr" @@ -149,7 +151,11 @@ func (f *Fetcher) FetchToBuffer(u url.URL, opts FetchOptions) ([]byte, error) { dest := new(bytes.Buffer) switch u.Scheme { case "http", "https": - err = f.fetchFromHTTP(u, dest, opts) + if strings.HasSuffix(u.Host, ".blob.core.windows.net") { + err = f.fetchFromAzureBlob(u, dest, opts) + } else { + err = f.fetchFromHTTP(u, dest, opts) + } case "tftp": err = f.fetchFromTFTP(u, dest, opts) case "data": @@ -210,6 +216,9 @@ func (f *Fetcher) Fetch(u url.URL, dest *os.File, opts FetchOptions) error { switch u.Scheme { case "http", "https": + if strings.HasSuffix(u.Host, ".blob.core.windows.net") { + return f.fetchFromAzureBlob(u, dest, opts) + } return f.fetchFromHTTP(u, dest, opts) case "tftp": return f.fetchFromTFTP(u, dest, opts) @@ -554,6 +563,61 @@ func (f *Fetcher) fetchFromS3WithCreds(ctx context.Context, dest s3target, input return nil } +func (f *Fetcher) fetchFromAzureBlob(u url.URL, dest io.Writer, opts FetchOptions) error { + // Read about NewDefaultAzureCredential https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential + // DefaultAzureCredential is a default credential chain for applications deployed to azure. + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + f.Logger.Debug("failed to obtain Azure credential: %v", err) + return fmt.Errorf("failed to obtain Azure credential: %w", err) + } + + // Create a context with timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Breakdown the URL into storage account, container, and file + storageAccount := fmt.Sprintf("%s://%s/", u.Scheme, u.Host) + pathSegments := strings.Split(strings.Trim(u.Path, "/"), "/") + if len(pathSegments) < 2 { + f.Logger.Debug("invalid URL path: %s", u.Path) + return fmt.Errorf("invalid URL path: %s", u.Path) + } + container := pathSegments[0] + file := pathSegments[1] + + // Create Azure Blob Storage client + storageClient, err := azblob.NewClient(storageAccount, cred, nil) + if err != nil { + f.Logger.Debug("failed to create azblob client: %v", err) + return fmt.Errorf("failed to create azblob client: %w", err) + } + + // Download the blob with retry logic + var downloadStream azblob.DownloadStreamResponse + for i := 0; i < 3; i++ { + downloadStream, err = storageClient.DownloadStream(ctx, container, file, nil) + if err == nil { + break + } + f.Logger.Debug("error downloading blob (attempt %d): %v", i+1, err) + time.Sleep(time.Duration(i+1) * time.Second) + } + if err != nil { + return fmt.Errorf("failed to download blob from container '%s', file '%s': %w", container, file, err) + } + defer downloadStream.Body.Close() + + // Process the downloaded blob + err = f.decompressCopyHashAndVerify(dest, downloadStream.Body, opts) + if err != nil { + f.Logger.Debug("Error processing downloaded blob: %v", err) + return fmt.Errorf("failed to process downloaded blob: %w", err) + } + + return nil +} + // uncompress will wrap the given io.Reader in a decompresser specified in the // FetchOptions, and return an io.ReadCloser with the decompressed data stream. func (f *Fetcher) uncompress(r io.Reader, opts FetchOptions) (io.ReadCloser, error) {