worker: add azure image upload target
This commit adds and implements org.osbuild.azure.image target. Let's talk about the already implemented org.osbuild.azure target firstly: The purpose of this target is to authenticate using the Azure Storage credentials and upload the image file as a Page Blob. Page Blob is basically an object in storage and it cannot be directly used to launch a VM. To achieve that, you need to define an actual Azure Image with the Page Blob attached. For the cloud API, we would like to create an actual Azure Image that is immediately available for new VMs. The new target accomplishes it. To achieve this, it must use a different authentication method: Azure OAuth. The other important difference is that currently, the credentials are stored on the worker and not in target options. This should lead to better security because we don't send the credentials over network. In the future, we would like to have credential-less setup using workers in Azure with the right IAM policies applied but this requires more investigation and is not implemented in this commit. Signed-off-by: Ondřej Budai <ondrej@budai.cz>
This commit is contained in:
parent
61e97372df
commit
2e39d629a9
52 changed files with 31234 additions and 0 deletions
|
|
@ -1,7 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
|
@ -9,9 +11,11 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/osbuild/osbuild-composer/internal/common"
|
||||
"github.com/osbuild/osbuild-composer/internal/distro"
|
||||
"github.com/osbuild/osbuild-composer/internal/osbuild"
|
||||
|
|
@ -29,6 +33,7 @@ type OSBuildJobImpl struct {
|
|||
Output string
|
||||
KojiServers map[string]koji.GSSAPICredentials
|
||||
GCPCredsPath string
|
||||
AzureCreds *azure.Credentials
|
||||
}
|
||||
|
||||
func packageMetadataToSignature(pkg osbuild.RPMPackageMetadata) *string {
|
||||
|
|
@ -297,6 +302,125 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
|
|||
ProjectID: g.GetProjectID(),
|
||||
}))
|
||||
|
||||
case *target.AzureImageTargetOptions:
|
||||
ctx := context.Background()
|
||||
|
||||
if impl.AzureCreds == nil {
|
||||
r = append(r, errors.New("osbuild job has org.osbuild.azure.image target but this worker doesn't have azure credentials"))
|
||||
continue
|
||||
}
|
||||
|
||||
c, err := azure.NewClient(*impl.AzureCreds, options.TenantID)
|
||||
if err != nil {
|
||||
r = append(r, err)
|
||||
continue
|
||||
}
|
||||
log.Print("[Azure] 🔑 Logged in Azure")
|
||||
|
||||
storageAccountTag := azure.Tag{
|
||||
Name: "imageBuilderStorageAccount",
|
||||
Value: fmt.Sprintf("location=%s", options.Location),
|
||||
}
|
||||
|
||||
storageAccount, err := c.GetResourceNameByTag(
|
||||
ctx,
|
||||
options.SubscriptionID,
|
||||
options.ResourceGroup,
|
||||
storageAccountTag,
|
||||
)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("searching for a storage account failed: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
if storageAccount == "" {
|
||||
log.Print("[Azure] 📦 Creating a new storage account")
|
||||
const storageAccountPrefix = "ib"
|
||||
storageAccount = azure.RandomStorageAccountName(storageAccountPrefix)
|
||||
|
||||
err := c.CreateStorageAccount(
|
||||
ctx,
|
||||
options.SubscriptionID,
|
||||
options.ResourceGroup,
|
||||
storageAccount,
|
||||
options.Location,
|
||||
storageAccountTag,
|
||||
)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("creating a new storage account failed: %v", err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Print("[Azure] 🔑📦 Retrieving a storage account key")
|
||||
storageAccessKey, err := c.GetStorageAccountKey(
|
||||
ctx,
|
||||
options.SubscriptionID,
|
||||
options.ResourceGroup,
|
||||
storageAccount,
|
||||
)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("retrieving the storage account key failed: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
azureStorageClient, err := azure.NewStorageClient(storageAccount, storageAccessKey)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("creating the storage client failed: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
storageContainer := "imagebuilder"
|
||||
|
||||
log.Print("[Azure] 📦 Ensuring that we have a storage container")
|
||||
err = azureStorageClient.CreateStorageContainerIfNotExist(ctx, storageAccount, storageContainer)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("cannot create a storage container: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
blobName := t.ImageName
|
||||
if !strings.HasSuffix(blobName, ".vhd") {
|
||||
blobName += ".vhd"
|
||||
}
|
||||
|
||||
log.Print("[Azure] ⬆ Uploading the image")
|
||||
err = azureStorageClient.UploadPageBlob(
|
||||
azure.BlobMetadata{
|
||||
StorageAccount: storageAccount,
|
||||
ContainerName: storageContainer,
|
||||
BlobName: blobName,
|
||||
},
|
||||
path.Join(outputDirectory, options.Filename),
|
||||
azure.DefaultUploadThreads,
|
||||
)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("uploading the image failed: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
log.Print("[Azure] 📝 Registering the image")
|
||||
err = c.RegisterImage(
|
||||
ctx,
|
||||
options.SubscriptionID,
|
||||
options.ResourceGroup,
|
||||
storageAccount,
|
||||
storageContainer,
|
||||
blobName,
|
||||
t.ImageName,
|
||||
options.Location,
|
||||
)
|
||||
if err != nil {
|
||||
r = append(r, fmt.Errorf("registering the image failed: %v", err))
|
||||
continue
|
||||
}
|
||||
|
||||
log.Print("[Azure] 🎉 Image uploaded and registered!")
|
||||
|
||||
targetResults = append(targetResults, target.NewAzureImageTargetResult(&target.AzureImageTargetResultOptions{
|
||||
ImageName: t.ImageName,
|
||||
}))
|
||||
|
||||
case *target.KojiTargetOptions:
|
||||
// Koji for some reason needs TLS renegotiation enabled.
|
||||
// Clone the default http transport and enable renegotiation.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/BurntSushi/toml"
|
||||
|
||||
"github.com/osbuild/osbuild-composer/internal/upload/azure"
|
||||
"github.com/osbuild/osbuild-composer/internal/upload/koji"
|
||||
"github.com/osbuild/osbuild-composer/internal/worker"
|
||||
)
|
||||
|
|
@ -86,6 +87,9 @@ func main() {
|
|||
GCP struct {
|
||||
Credentials string `toml:"credentials"`
|
||||
} `toml:"gcp"`
|
||||
Azure *struct {
|
||||
Credentials string `toml:"credentials"`
|
||||
} `toml:"azure"`
|
||||
}
|
||||
var unix bool
|
||||
flag.BoolVar(&unix, "unix", false, "Interpret 'address' as a path to a unix domain socket instead of a network address")
|
||||
|
|
@ -154,12 +158,24 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
// Load Azure credentials early. If the credentials file is malformed,
|
||||
// we can report the issue early instead of waiting for the first osbuild
|
||||
// job with the org.osbuild.azure.image target.
|
||||
var azureCredentials *azure.Credentials
|
||||
if config.Azure != nil {
|
||||
azureCredentials, err = azure.ParseAzureCredentialsFile(config.Azure.Credentials)
|
||||
if err != nil {
|
||||
log.Fatalf("cannot load azure credentials: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
jobImpls := map[string]JobImplementation{
|
||||
"osbuild": &OSBuildJobImpl{
|
||||
Store: store,
|
||||
Output: output,
|
||||
KojiServers: kojiServers,
|
||||
GCPCredsPath: config.GCP.Credentials,
|
||||
AzureCreds: azureCredentials,
|
||||
},
|
||||
"osbuild-koji": &OSBuildKojiJobImpl{
|
||||
Store: store,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue