diff --git a/bazel/toolchains/go_module_deps.bzl b/bazel/toolchains/go_module_deps.bzl
index 284e44547..10e629abf 100644
--- a/bazel/toolchains/go_module_deps.bzl
+++ b/bazel/toolchains/go_module_deps.bzl
@@ -604,8 +604,8 @@ def go_dependencies():
build_file_generation = "on",
build_file_proto_mode = "disable_global",
importpath = "github.com/Azure/azure-sdk-for-go/sdk/azcore",
- sum = "h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY=",
- version = "v1.4.0",
+ sum = "h1:xGLAFFd9D3iLGxYiUGPdITSzsFmU1K8VtfuUHWAoN7M=",
+ version = "v1.5.0",
)
go_repository(
name = "com_github_azure_azure_sdk_for_go_sdk_azidentity",
@@ -620,8 +620,8 @@ def go_dependencies():
build_file_generation = "on",
build_file_proto_mode = "disable_global",
importpath = "github.com/Azure/azure-sdk-for-go/sdk/internal",
- sum = "h1:leh5DwKv6Ihwi+h60uHtn6UWAxBbZ0q8DwQVMzf61zw=",
- version = "v1.2.0",
+ sum = "h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=",
+ version = "v1.3.0",
)
go_repository(
@@ -701,8 +701,8 @@ def go_dependencies():
build_file_generation = "on",
build_file_proto_mode = "disable_global",
importpath = "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob",
- sum = "h1:YvQv9Mz6T8oR5ypQOL6erY0Z5t71ak1uHV4QFokCOZk=",
- version = "v0.6.1",
+ sum = "h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY=",
+ version = "v1.0.0",
)
go_repository(
name = "com_github_azure_azure_service_bus_go",
diff --git a/go.mod b/go.mod
index df2887e7a..c4584d648 100644
--- a/go.mod
+++ b/go.mod
@@ -45,13 +45,13 @@ require (
cloud.google.com/go/secretmanager v1.10.0
cloud.google.com/go/storage v1.30.1
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.11.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights v1.0.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2 v2.1.0
- github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1
+ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/aws/aws-sdk-go-v2 v1.17.7
github.com/aws/aws-sdk-go-v2/config v1.18.19
@@ -129,7 +129,7 @@ require (
cloud.google.com/go/iam v0.12.0 // indirect
cloud.google.com/go/longrunning v0.4.1 // indirect
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
diff --git a/go.sum b/go.sum
index 8878572cd..0932eccf3 100644
--- a/go.sum
+++ b/go.sum
@@ -89,12 +89,12 @@ github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 h1:xGLAFFd9D3iLGxYiUGPdITSzsFmU1K8VtfuUHWAoN7M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 h1:leh5DwKv6Ihwi+h60uHtn6UWAxBbZ0q8DwQVMzf61zw=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.11.0 h1:82w8tzLcOwDP/Q35j/wEBPt0n0kVC3cjtPdD62G8UAk=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.11.0/go.mod h1:S78i9yTr4o/nXlH76bKjGUye9Z2wSxO5Tz7GoDr4vfI=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 h1:Lg6BW0VPmCwcMlvOviL3ruHFO+H9tZNqscK0AeuFjGM=
@@ -110,8 +110,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2 v2.1.0 h1:mk57wRUA8fyjFxVcPPGv4shLcWDXPFYokTJL9zJxQtE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2 v2.1.0/go.mod h1:mU96hbp8qJDA9OzTV1Ji7wCyPyaqC5kI6ZPsZfJ8sE4=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1 h1:YvQv9Mz6T8oR5ypQOL6erY0Z5t71ak1uHV4QFokCOZk=
-github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.6.1/go.mod h1:c6WvOhtmjNUWbLfOG1qxM/q0SPvQNSVJvolm+C52dIU=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0 h1:u/LLAOFgsMv7HmNL4Qufg58y+qElGOt5qv0z1mURkRY=
+github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0/go.mod h1:2e8rMJtl2+2j+HXbTBwnyGpm5Nou7KhvSfxOq8JpTag=
github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0=
github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
diff --git a/hack/go.mod b/hack/go.mod
index 32dea1cdb..a17ec16aa 100644
--- a/hack/go.mod
+++ b/hack/go.mod
@@ -60,9 +60,9 @@ require (
cloud.google.com/go/compute/metadata v0.2.3 // indirect
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights v1.0.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4 v4.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2 v2.1.0 // indirect
diff --git a/hack/go.sum b/hack/go.sum
index d93fa0744..28ba7ef91 100644
--- a/hack/go.sum
+++ b/hack/go.sum
@@ -81,12 +81,12 @@ github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0 h1:rTnT/Jrcm+figWlYz4Ixzt0SJVR2cMC8lvZcimipiEY=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 h1:xGLAFFd9D3iLGxYiUGPdITSzsFmU1K8VtfuUHWAoN7M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0 h1:leh5DwKv6Ihwi+h60uHtn6UWAxBbZ0q8DwQVMzf61zw=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights v1.0.0 h1:BpGGvzarSyE7kQF1x1hptUcGmNzZEE3yYI+uqBSNRxk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/applicationinsights/armapplicationinsights v1.0.0/go.mod h1:1ijUM40peD7YK5MFEJja2wjjp4eimFNWv0NXoY3nsZM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v3 v3.0.1 h1:H3g2mkmu105ON0c/Gqx3Bm+bzoIijLom8LmV9Gjn7X0=
diff --git a/image/BUILD.bazel b/image/BUILD.bazel
new file mode 100644
index 000000000..e69de29bb
diff --git a/image/README.md b/image/README.md
index 4bbf42c5e..2f9619821 100644
--- a/image/README.md
+++ b/image/README.md
@@ -57,7 +57,6 @@
jq \
mtools \
ovmf \
- python3-crc32c \
python3-pefile \
python3-pyelftools \
python3-setuptools \
@@ -177,6 +176,8 @@ secure-boot/azure/delete.sh --name "${AZURE_DISK_NAME}-setup-secure-boot"
## Upload to CSP
+Warning! Never set `--version` to a value that is already used for a release image.
+
AWS
@@ -188,19 +189,9 @@ secure-boot/azure/delete.sh --name "${AZURE_DISK_NAME}-setup-secure-boot"
- `pki_prod` is used for release images
```sh
-# set these variables
-export AWS_IMAGE_NAME= # e.g. "constellation-v1.0.0"
-export PKI=${PWD}/pki
-
-export AWS_REGION=eu-central-1
-export AWS_REPLICATION_REGIONS="us-east-2"
-export AWS_BUCKET=constellation-images
-export AWS_EFIVARS_PATH=${PWD}/mkosi.output.aws/fedora~37/efivars.bin
-export AWS_IMAGE_PATH=${PWD}/mkosi.output.aws/fedora~37/image.raw
-export AWS_IMAGE_FILENAME=image-$(date +%s).raw
-export AWS_JSON_OUTPUT=${PWD}/mkosi.output.aws/fedora~37/image-upload.json
-secure-boot/aws/create_uefivars.sh "${AWS_EFIVARS_PATH}"
-upload/upload_aws.sh
+# Warning! Never set `--version` to a value that is already used for a release image.
+# Instead, use a `ref` that corresponds to your branch name.
+bazel run //image/upload -- aws --verbose --raw-image mkosi.output.aws/fedora~37/image.raw --variant "" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
@@ -216,20 +207,12 @@ upload/upload_aws.sh
- `pki_prod` is used for release images
```sh
-# set these variables
-export GCP_IMAGE_FAMILY= # e.g. "constellation"
-export GCP_IMAGE_NAME= # e.g. "constellation-v1.0.0"
-export PKI=${PWD}/pki
-
-export GCP_PROJECT=constellation-images
-export GCP_REGION=europe-west3
-export GCP_BUCKET=constellation-images
export GCP_RAW_IMAGE_PATH=${PWD}/mkosi.output.gcp/fedora~37/image.raw
-export GCP_IMAGE_FILENAME=$(date +%s).tar.gz
export GCP_IMAGE_PATH=${PWD}/mkosi.output.gcp/fedora~37/image.tar.gz
-export GCP_JSON_OUTPUT=${PWD}/mkosi.output.gcp/fedora~37/image-upload.json
upload/pack.sh gcp ${GCP_RAW_IMAGE_PATH} ${GCP_IMAGE_PATH}
-upload/upload_gcp.sh
+# Warning! Never set `--version` to a value that is already used for a release image.
+# Instead, use a `ref` that corresponds to your branch name.
+bazel run //image/upload -- gcp --verbose --raw-image "${GCP_IMAGE_PATH}" --variant "sev-es" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
@@ -247,31 +230,12 @@ Note:
- Optional (if Secure Boot should be enabled) [Prepare virtual machine guest state (VMGS) with customized NVRAM or use existing VMGS blob](#azure-secure-boot)
```sh
-# set these variables
-export AZURE_GALLERY_NAME= # e.g. "Constellation"
-export AZURE_IMAGE_DEFINITION= # e.g. "constellation"
-export AZURE_IMAGE_VERSION= # e.g. "1.0.0"
-# Set this variable to a path if you want to use Secure Boot.
-# Otherwise, set it to export AZURE_VMGS_PATH=
-export AZURE_VMGS_PATH= # e.g. nothing OR "path/to/ConfidentialVM.vmgs"
-# AZURE_SECURITY_TYPE can be one of
-# - "ConfidentialVMSupported" (ConfidentialVM with secure boot disabled),
-# - "ConfidentialVM" (ConfidentialVM with Secure Boot) or
-# - TrustedLaunch" (Trusted Launch with or without Secure Boot)
-export AZURE_SECURITY_TYPE=ConfidentialVMSupported
-
-export AZURE_RESOURCE_GROUP_NAME=constellation-images
-export AZURE_REGION=northeurope
-export AZURE_REPLICATION_REGIONS="northeurope eastus westeurope westus"
-export AZURE_IMAGE_OFFER=constellation
-export AZURE_SKU=${AZURE_IMAGE_DEFINITION}
-export AZURE_PUBLISHER=edgelesssys
-export AZURE_DISK_NAME=constellation-$(date +%s)
export AZURE_RAW_IMAGE_PATH=${PWD}/mkosi.output.azure/fedora~37/image.raw
export AZURE_IMAGE_PATH=${PWD}/mkosi.output.azure/fedora~37/image.vhd
-export AZURE_JSON_OUTPUT=${PWD}/mkosi.output.azure/fedora~37/image-upload.json
upload/pack.sh azure "${AZURE_RAW_IMAGE_PATH}" "${AZURE_IMAGE_PATH}"
-upload/upload_azure.sh -g --disk-name "${AZURE_DISK_NAME}" "${AZURE_VMGS_PATH}"
+# Warning! Never set `--version` to a value that is already used for a release image.
+# Instead, use a `ref` that corresponds to your branch name.
+bazel run //image/upload -- azure --verbose --raw-image "${AZURE_IMAGE_PATH}" --variant "cvm" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
@@ -288,15 +252,9 @@ Note:
- Login to AWS (see [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html))
```sh
-# set these variables
-export REF= # e.g. feat-xyz (branch name encoded with dashes)
-export STREAM= # e.g. "nightly", "debug", "stable" (depends on the type of image and if it is a release)
-export IMAGE_VERSION= # e.g. v2.1.0" or output of pseudo-version tool
-export OPENSTACK_BUCKET=cdn-constellation-backend
-export OPENSTACK_BASE_URL="https://cdn.confidential.cloud"
-export OPENSTACK_IMAGE_PATH=${PWD}/mkosi.output.qemu/fedora~37/image.raw
-export OPENSTACK_JSON_OUTPUT=${PWD}/mkosi.output.qemu/fedora~37/image-upload.json
-upload/upload_openstack.sh
+# Warning! Never set `--version` to a value that is already used for a release image.
+# Instead, use a `ref` that corresponds to your branch name.
+bazel run //image/upload -- openstack --verbose --raw-image mkosi.output.openstack/fedora~37/image.raw --variant "sev" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
@@ -308,15 +266,9 @@ upload/upload_openstack.sh
- Login to AWS (see [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html))
```sh
-# set these variables
-export REF= # e.g. feat-xyz (branch name encoded with dashes)
-export STREAM= # e.g. "nightly", "debug", "stable" (depends on the type of image and if it is a release)
-export IMAGE_VERSION= # e.g. v2.1.0" or output of pseudo-version tool
-export QEMU_BUCKET=cdn-constellation-backend
-export QEMU_BASE_URL="https://cdn.confidential.cloud"
-export QEMU_IMAGE_PATH=${PWD}/mkosi.output.qemu/fedora~37/image.raw
-export QEMU_JSON_OUTPUT=${PWD}/mkosi.output.qemu/fedora~37/image-upload.json
-upload/upload_qemu.sh
+# Warning! Never set `--version` to a value that is already used for a release image.
+# Instead, use a `ref` that corresponds to your branch name.
+bazel run //image/upload -- qemu --verbose --raw-image mkosi.output.qemu/fedora~37/image.raw --variant "default" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
diff --git a/image/upload/BUILD.bazel b/image/upload/BUILD.bazel
new file mode 100644
index 000000000..8eef54f0e
--- /dev/null
+++ b/image/upload/BUILD.bazel
@@ -0,0 +1,18 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+ name = "upload_lib",
+ srcs = ["upload.go"],
+ importpath = "github.com/edgelesssys/constellation/v2/image/upload",
+ visibility = ["//visibility:private"],
+ deps = [
+ "//image/upload/internal/cmd",
+ "@com_github_spf13_cobra//:cobra",
+ ],
+)
+
+go_binary(
+ name = "upload",
+ embed = [":upload_lib"],
+ visibility = ["//visibility:public"],
+)
diff --git a/image/upload/delete_aws.sh b/image/upload/delete_aws.sh
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/image/upload/delete_aws.sh
@@ -0,0 +1 @@
+
diff --git a/image/upload/internal/cmd/BUILD.bazel b/image/upload/internal/cmd/BUILD.bazel
new file mode 100644
index 000000000..5b1d77371
--- /dev/null
+++ b/image/upload/internal/cmd/BUILD.bazel
@@ -0,0 +1,35 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "cmd",
+ srcs = [
+ "api.go",
+ "aws.go",
+ "azure.go",
+ "flags.go",
+ "gcp.go",
+ "must.go",
+ "nop.go",
+ "openstack.go",
+ "qemu.go",
+ "secureboot.go",
+ "upload.go",
+ ],
+ importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd",
+ visibility = ["//image/upload:__subpackages__"],
+ deps = [
+ "//internal/cloud/cloudprovider",
+ "//internal/logger",
+ "//internal/osimage",
+ "//internal/osimage/archive",
+ "//internal/osimage/aws",
+ "//internal/osimage/azure",
+ "//internal/osimage/gcp",
+ "//internal/osimage/nop",
+ "//internal/osimage/secureboot",
+ "//internal/versionsapi",
+ "@com_github_spf13_afero//:afero",
+ "@com_github_spf13_cobra//:cobra",
+ "@org_uber_go_zap//zapcore",
+ ],
+)
diff --git a/image/upload/internal/cmd/api.go b/image/upload/internal/cmd/api.go
new file mode 100644
index 000000000..f2b3445b3
--- /dev/null
+++ b/image/upload/internal/cmd/api.go
@@ -0,0 +1,25 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "context"
+ "io"
+
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/versionsapi"
+)
+
+type archivist interface {
+ Archive(ctx context.Context,
+ version versionsapi.Version, csp, variant string, img io.Reader,
+ ) (string, error)
+}
+
+type uploader interface {
+ Upload(ctx context.Context, req *osimage.UploadRequest) (map[string]string, error)
+}
diff --git a/image/upload/internal/cmd/aws.go b/image/upload/internal/cmd/aws.go
new file mode 100644
index 000000000..9817420db
--- /dev/null
+++ b/image/upload/internal/cmd/aws.go
@@ -0,0 +1,97 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/osimage/archive"
+ awsupload "github.com/edgelesssys/constellation/v2/internal/osimage/aws"
+ "github.com/spf13/cobra"
+)
+
+// NewAWSCmd returns the command that uploads an OS image to AWS.
+func NewAWSCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "aws",
+ Short: "Upload OS image to AWS",
+ Long: "Upload OS image to AWS.",
+ Args: cobra.ExactArgs(0),
+ RunE: runAWS,
+ }
+
+ cmd.Flags().String("aws-region", "eu-central-1", "AWS region used during AMI creation")
+ cmd.Flags().String("aws-bucket", "constellation-images", "S3 bucket used during AMI creation")
+ return cmd
+}
+
+func runAWS(cmd *cobra.Command, _ []string) error {
+ workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
+ if len(workdir) > 0 {
+ must(os.Chdir(workdir))
+ }
+
+ flags, err := parseAWSFlags(cmd)
+ if err != nil {
+ return err
+ }
+ log := logger.New(logger.PlainLog, flags.logLevel)
+ log.Debugf("Parsed flags: %+v", flags)
+
+ archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
+ if err != nil {
+ return err
+ }
+
+ uploadC, err := awsupload.New(flags.awsRegion, flags.awsBucket, log)
+ if err != nil {
+ return fmt.Errorf("uploading image: %w", err)
+ }
+
+ file, err := os.Open(flags.rawImage)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening image file %w", err)
+ }
+ defer file.Close()
+ size, err := file.Seek(0, io.SeekEnd)
+ if err != nil {
+ return err
+ }
+ if _, err := file.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ out := cmd.OutOrStdout()
+ if len(flags.out) > 0 {
+ outF, err := os.Create(flags.out)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening output file %w", err)
+ }
+ defer outF.Close()
+ out = outF
+ }
+
+ sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
+ if err != nil {
+ return err
+ }
+
+ uploadReq := &osimage.UploadRequest{
+ Provider: flags.provider,
+ Version: flags.version,
+ Variant: flags.variant,
+ SBDatabase: sbDatabase,
+ UEFIVarStore: uefiVarStore,
+ Size: size,
+ Timestamp: flags.timestamp,
+ Image: file,
+ }
+ return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
+}
diff --git a/image/upload/internal/cmd/azure.go b/image/upload/internal/cmd/azure.go
new file mode 100644
index 000000000..aa760c069
--- /dev/null
+++ b/image/upload/internal/cmd/azure.go
@@ -0,0 +1,98 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/osimage/archive"
+ azureupload "github.com/edgelesssys/constellation/v2/internal/osimage/azure"
+ "github.com/spf13/cobra"
+)
+
+// NewAzureCmd returns the command that uploads an OS image to Azure.
+func NewAzureCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "azure",
+ Short: "Upload OS image to Azure",
+ Long: "Upload OS image to Azure.",
+ Args: cobra.ExactArgs(0),
+ RunE: runAzure,
+ }
+
+ cmd.Flags().String("az-subscription", "0d202bbb-4fa7-4af8-8125-58c269a05435", "Azure subscription to use")
+ cmd.Flags().String("az-location", "northeurope", "Azure location to use")
+ cmd.Flags().String("az-resource-group", "constellation-images", "Azure resource group to use")
+ return cmd
+}
+
+func runAzure(cmd *cobra.Command, _ []string) error {
+ workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
+ if len(workdir) > 0 {
+ must(os.Chdir(workdir))
+ }
+
+ flags, err := parseAzureFlags(cmd)
+ if err != nil {
+ return err
+ }
+ log := logger.New(logger.PlainLog, flags.logLevel)
+ log.Debugf("Parsed flags: %+v", flags)
+
+ archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
+ if err != nil {
+ return err
+ }
+
+ uploadC, err := azureupload.New(flags.azSubscription, flags.azLocation, flags.azResourceGroup, log)
+ if err != nil {
+ return fmt.Errorf("uploading image: %w", err)
+ }
+
+ file, err := os.Open(flags.rawImage)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening image file %w", err)
+ }
+ defer file.Close()
+ size, err := file.Seek(0, io.SeekEnd)
+ if err != nil {
+ return err
+ }
+ if _, err := file.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ out := cmd.OutOrStdout()
+ if len(flags.out) > 0 {
+ outF, err := os.Create(flags.out)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening output file %w", err)
+ }
+ defer outF.Close()
+ out = outF
+ }
+
+ sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
+ if err != nil {
+ return err
+ }
+
+ uploadReq := &osimage.UploadRequest{
+ Provider: flags.provider,
+ Version: flags.version,
+ Variant: flags.variant,
+ SBDatabase: sbDatabase,
+ UEFIVarStore: uefiVarStore,
+ Size: size,
+ Timestamp: flags.timestamp,
+ Image: file,
+ }
+ return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
+}
diff --git a/image/upload/internal/cmd/flags.go b/image/upload/internal/cmd/flags.go
new file mode 100644
index 000000000..06b3fbf42
--- /dev/null
+++ b/image/upload/internal/cmd/flags.go
@@ -0,0 +1,200 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
+ "github.com/edgelesssys/constellation/v2/internal/versionsapi"
+ "github.com/spf13/cobra"
+ "go.uber.org/zap/zapcore"
+)
+
+type commonFlags struct {
+ rawImage string
+ pki string
+ provider cloudprovider.Provider
+ variant string
+ version versionsapi.Version
+ timestamp time.Time
+ region string
+ bucket string
+ out string
+ logLevel zapcore.Level
+}
+
+func parseCommonFlags(cmd *cobra.Command) (commonFlags, error) {
+ workspaceDir := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
+ rawImage, err := cmd.Flags().GetString("raw-image")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ pki, err := cmd.Flags().GetString("pki")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ if pki == "" {
+ pki = filepath.Join(workspaceDir, "image/pki")
+ }
+ variant, err := cmd.Flags().GetString("variant")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ version, err := cmd.Flags().GetString("version")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ ver, err := versionsapi.NewVersionFromShortPath(version, versionsapi.VersionKindImage)
+ if err != nil {
+ return commonFlags{}, err
+ }
+ timestamp, err := cmd.Flags().GetString("timestamp")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ if timestamp == "" {
+ timestamp = time.Now().Format("2006-01-02T15:04:05Z07:00")
+ }
+ timestmp, err := time.Parse("2006-01-02T15:04:05Z07:00", timestamp)
+ if err != nil {
+ return commonFlags{}, err
+ }
+ region, err := cmd.Flags().GetString("region")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ bucket, err := cmd.Flags().GetString("bucket")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ out, err := cmd.Flags().GetString("out")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ verbose, err := cmd.Flags().GetBool("verbose")
+ if err != nil {
+ return commonFlags{}, err
+ }
+ logLevel := zapcore.InfoLevel
+ if verbose {
+ logLevel = zapcore.DebugLevel
+ }
+
+ return commonFlags{
+ rawImage: rawImage,
+ pki: pki,
+ variant: variant,
+ version: ver,
+ timestamp: timestmp,
+ region: region,
+ bucket: bucket,
+ out: out,
+ logLevel: logLevel,
+ }, nil
+}
+
+type awsFlags struct {
+ commonFlags
+ awsRegion string
+ awsBucket string
+}
+
+func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
+ common, err := parseCommonFlags(cmd)
+ if err != nil {
+ return awsFlags{}, err
+ }
+
+ awsRegion, err := cmd.Flags().GetString("aws-region")
+ if err != nil {
+ return awsFlags{}, err
+ }
+ awsBucket, err := cmd.Flags().GetString("aws-bucket")
+ if err != nil {
+ return awsFlags{}, err
+ }
+
+ common.provider = cloudprovider.AWS
+ return awsFlags{
+ commonFlags: common,
+ awsRegion: awsRegion,
+ awsBucket: awsBucket,
+ }, nil
+}
+
+type azureFlags struct {
+ commonFlags
+ azSubscription string
+ azLocation string
+ azResourceGroup string
+}
+
+func parseAzureFlags(cmd *cobra.Command) (azureFlags, error) {
+ common, err := parseCommonFlags(cmd)
+ if err != nil {
+ return azureFlags{}, err
+ }
+
+ azSubscription, err := cmd.Flags().GetString("az-subscription")
+ if err != nil {
+ return azureFlags{}, err
+ }
+ azLocation, err := cmd.Flags().GetString("az-location")
+ if err != nil {
+ return azureFlags{}, err
+ }
+ azResourceGroup, err := cmd.Flags().GetString("az-resource-group")
+ if err != nil {
+ return azureFlags{}, err
+ }
+
+ common.provider = cloudprovider.Azure
+ return azureFlags{
+ commonFlags: common,
+ azSubscription: azSubscription,
+ azLocation: azLocation,
+ azResourceGroup: azResourceGroup,
+ }, nil
+}
+
+type gcpFlags struct {
+ commonFlags
+ gcpProject string
+ gcpLocation string
+ gcpBucket string
+}
+
+func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
+ common, err := parseCommonFlags(cmd)
+ if err != nil {
+ return gcpFlags{}, err
+ }
+
+ gcpProject, err := cmd.Flags().GetString("gcp-project")
+ if err != nil {
+ return gcpFlags{}, err
+ }
+ gcpLocation, err := cmd.Flags().GetString("gcp-location")
+ if err != nil {
+ return gcpFlags{}, err
+ }
+ gcpBucket, err := cmd.Flags().GetString("gcp-bucket")
+ if err != nil {
+ return gcpFlags{}, err
+ }
+
+ common.provider = cloudprovider.GCP
+ return gcpFlags{
+ commonFlags: common,
+ gcpProject: gcpProject,
+ gcpLocation: gcpLocation,
+ gcpBucket: gcpBucket,
+ }, nil
+}
diff --git a/image/upload/internal/cmd/gcp.go b/image/upload/internal/cmd/gcp.go
new file mode 100644
index 000000000..b562e9ee1
--- /dev/null
+++ b/image/upload/internal/cmd/gcp.go
@@ -0,0 +1,98 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/osimage/archive"
+ gcpupload "github.com/edgelesssys/constellation/v2/internal/osimage/gcp"
+ "github.com/spf13/cobra"
+)
+
+// NewGCPCommand returns the command that uploads an OS image to GCP.
+func NewGCPCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "gcp",
+ Short: "Upload OS image to GCP",
+ Long: "Upload OS image to GCP.",
+ Args: cobra.ExactArgs(0),
+ RunE: runGCP,
+ }
+
+ cmd.Flags().String("gcp-project", "constellation-images", "GCP project to use")
+ cmd.Flags().String("gcp-location", "europe-west3", "GCP location to use")
+ cmd.Flags().String("gcp-bucket", "constellation-images", "GCP bucket to use")
+ return cmd
+}
+
+func runGCP(cmd *cobra.Command, _ []string) error {
+ workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
+ if len(workdir) > 0 {
+ must(os.Chdir(workdir))
+ }
+
+ flags, err := parseGCPFlags(cmd)
+ if err != nil {
+ return err
+ }
+ log := logger.New(logger.PlainLog, flags.logLevel)
+ log.Debugf("Parsed flags: %+v", flags)
+
+ archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
+ if err != nil {
+ return err
+ }
+
+ uploadC, err := gcpupload.New(cmd.Context(), flags.gcpProject, flags.gcpLocation, flags.gcpBucket, log)
+ if err != nil {
+ return fmt.Errorf("uploading image: %w", err)
+ }
+
+ file, err := os.Open(flags.rawImage)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening image file %w", err)
+ }
+ defer file.Close()
+ size, err := file.Seek(0, io.SeekEnd)
+ if err != nil {
+ return err
+ }
+ if _, err := file.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ out := cmd.OutOrStdout()
+ if len(flags.out) > 0 {
+ outF, err := os.Create(flags.out)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening output file %w", err)
+ }
+ defer outF.Close()
+ out = outF
+ }
+
+ sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
+ if err != nil {
+ return err
+ }
+
+ uploadReq := &osimage.UploadRequest{
+ Provider: flags.provider,
+ Version: flags.version,
+ Variant: flags.variant,
+ SBDatabase: sbDatabase,
+ UEFIVarStore: uefiVarStore,
+ Size: size,
+ Timestamp: flags.timestamp,
+ Image: file,
+ }
+ return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
+}
diff --git a/image/upload/internal/cmd/must.go b/image/upload/internal/cmd/must.go
new file mode 100644
index 000000000..fb26f2df0
--- /dev/null
+++ b/image/upload/internal/cmd/must.go
@@ -0,0 +1,13 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+func must(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/image/upload/internal/cmd/nop.go b/image/upload/internal/cmd/nop.go
new file mode 100644
index 000000000..abc415392
--- /dev/null
+++ b/image/upload/internal/cmd/nop.go
@@ -0,0 +1,81 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/osimage/archive"
+ nopupload "github.com/edgelesssys/constellation/v2/internal/osimage/nop"
+ "github.com/spf13/cobra"
+)
+
+func runNOP(cmd *cobra.Command, provider cloudprovider.Provider, _ []string) error {
+ workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
+ if len(workdir) > 0 {
+ must(os.Chdir(workdir))
+ }
+
+ flags, err := parseCommonFlags(cmd)
+ if err != nil {
+ return err
+ }
+ flags.provider = provider
+ log := logger.New(logger.PlainLog, flags.logLevel)
+ log.Debugf("Parsed flags: %+v", flags)
+
+ archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
+ if err != nil {
+ return err
+ }
+
+ uploadC := nopupload.New(log)
+
+ file, err := os.Open(flags.rawImage)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening image file %w", err)
+ }
+ defer file.Close()
+ size, err := file.Seek(0, io.SeekEnd)
+ if err != nil {
+ return err
+ }
+ if _, err := file.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ out := cmd.OutOrStdout()
+ if len(flags.out) > 0 {
+ outF, err := os.Create(flags.out)
+ if err != nil {
+ return fmt.Errorf("uploading image: opening output file %w", err)
+ }
+ defer outF.Close()
+ out = outF
+ }
+
+ sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
+ if err != nil {
+ return err
+ }
+
+ uploadReq := &osimage.UploadRequest{
+ Provider: flags.provider,
+ Version: flags.version,
+ Variant: flags.variant,
+ SBDatabase: sbDatabase,
+ UEFIVarStore: uefiVarStore,
+ Size: size,
+ Timestamp: flags.timestamp,
+ Image: file,
+ }
+ return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
+}
diff --git a/image/upload/internal/cmd/openstack.go b/image/upload/internal/cmd/openstack.go
new file mode 100644
index 000000000..6696f4310
--- /dev/null
+++ b/image/upload/internal/cmd/openstack.go
@@ -0,0 +1,29 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
+ "github.com/spf13/cobra"
+)
+
+// NewOpenStackCmd returns the command that uploads an OS image to OpenStack.
+func NewOpenStackCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "openstack",
+ Short: "Upload OS image to OpenStack",
+ Long: "Upload OS image to OpenStack.",
+ Args: cobra.ExactArgs(0),
+ RunE: runOpenStack,
+ }
+
+ return cmd
+}
+
+func runOpenStack(cmd *cobra.Command, args []string) error {
+ return runNOP(cmd, cloudprovider.OpenStack, args)
+}
diff --git a/image/upload/internal/cmd/qemu.go b/image/upload/internal/cmd/qemu.go
new file mode 100644
index 000000000..b250620d4
--- /dev/null
+++ b/image/upload/internal/cmd/qemu.go
@@ -0,0 +1,29 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
+ "github.com/spf13/cobra"
+)
+
+// NewQEMUCmd returns the command that uploads an OS image to QEMU.
+func NewQEMUCmd() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "qemu",
+ Short: "Upload OS image to QEMU",
+ Long: "Upload OS image to QEMU.",
+ Args: cobra.ExactArgs(0),
+ RunE: runQEMU,
+ }
+
+ return cmd
+}
+
+func runQEMU(cmd *cobra.Command, args []string) error {
+ return runNOP(cmd, cloudprovider.QEMU, args)
+}
diff --git a/image/upload/internal/cmd/secureboot.go b/image/upload/internal/cmd/secureboot.go
new file mode 100644
index 000000000..a73f2af0f
--- /dev/null
+++ b/image/upload/internal/cmd/secureboot.go
@@ -0,0 +1,44 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
+ "github.com/spf13/afero"
+)
+
+func loadSecureBootKeys(basePath string) (secureboot.Database, secureboot.UEFIVarStore, error) {
+ platformKeyCert := filepath.Join(basePath, "PK.cer")
+ keyExchangeKeyCerts := []string{
+ filepath.Join(basePath, "KEK.cer"),
+ filepath.Join(basePath, "MicCorKEKCA2011_2011-06-24.crt"),
+ }
+ signatureDBCerts := []string{
+ filepath.Join(basePath, "db.cer"),
+ filepath.Join(basePath, "MicWinProPCA2011_2011-10-19.crt"),
+ filepath.Join(basePath, "MicCorUEFCA2011_2011-06-27.crt"),
+ }
+ sbDatabase, err := secureboot.DatabaseFromFiles(afero.NewOsFs(), platformKeyCert, keyExchangeKeyCerts, signatureDBCerts)
+ if err != nil {
+ return secureboot.Database{},
+ secureboot.UEFIVarStore{},
+ fmt.Errorf("preparing secure boot database: %w", err)
+ }
+ platformKeyESL := filepath.Join(basePath, "PK.esl")
+ keyExchangeKeyESL := filepath.Join(basePath, "KEK.esl")
+ signatureDBESL := filepath.Join(basePath, "db.esl")
+ uefiVarStore, err := secureboot.VarStoreFromFiles(afero.NewOsFs(), platformKeyESL, keyExchangeKeyESL, signatureDBESL, "")
+ if err != nil {
+ return secureboot.Database{},
+ secureboot.UEFIVarStore{},
+ fmt.Errorf("preparing secure boot variable store: %w", err)
+ }
+ return sbDatabase, uefiVarStore, nil
+}
diff --git a/image/upload/internal/cmd/upload.go b/image/upload/internal/cmd/upload.go
new file mode 100644
index 000000000..719daf293
--- /dev/null
+++ b/image/upload/internal/cmd/upload.go
@@ -0,0 +1,66 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/versionsapi"
+)
+
+func uploadImage(ctx context.Context, archiveC archivist, uploadC uploader, req *osimage.UploadRequest, out io.Writer) error {
+ // upload to S3 archive
+ archiveURL, err := archiveC.Archive(ctx, req.Version, req.Provider.String(), req.Variant, req.Image)
+ if err != nil {
+ return err
+ }
+ // rewind reader so we can read again
+ if _, err := req.Image.Seek(0, io.SeekStart); err != nil {
+ return err
+ }
+ // upload to CSP
+ imageReferences, err := uploadC.Upload(ctx, req)
+ if err != nil {
+ return err
+ }
+ if len(imageReferences) == 0 {
+ imageReferences = map[string]string{
+ req.Variant: archiveURL,
+ }
+ }
+
+ imageInfo := versionsapi.ImageInfo{
+ Ref: req.Version.Ref,
+ Stream: req.Version.Stream,
+ Version: req.Version.Version,
+ }
+ switch req.Provider {
+ case cloudprovider.AWS:
+ imageInfo.AWS = imageReferences
+ case cloudprovider.Azure:
+ imageInfo.Azure = imageReferences
+ case cloudprovider.GCP:
+ imageInfo.GCP = imageReferences
+ case cloudprovider.OpenStack:
+ imageInfo.OpenStack = imageReferences
+ case cloudprovider.QEMU:
+ imageInfo.QEMU = imageReferences
+ default:
+ return fmt.Errorf("uploading image: cloud provider %s is not yet supported", req.Provider.String())
+ }
+
+ if err := json.NewEncoder(out).Encode(imageInfo); err != nil {
+ return fmt.Errorf("uploading image: marshaling output: %w", err)
+ }
+
+ return nil
+}
diff --git a/image/upload/upload.go b/image/upload/upload.go
new file mode 100644
index 000000000..fe0b79e8e
--- /dev/null
+++ b/image/upload/upload.go
@@ -0,0 +1,100 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+// upload uploads os images.
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+
+ "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd"
+ "github.com/spf13/cobra"
+)
+
+func main() {
+ if err := execute(); err != nil {
+ os.Exit(1)
+ }
+}
+
+func execute() error {
+ rootCmd := newRootCmd()
+ ctx, cancel := signalContext(context.Background(), os.Interrupt)
+ defer cancel()
+ return rootCmd.ExecuteContext(ctx)
+}
+
+func newRootCmd() *cobra.Command {
+ rootCmd := &cobra.Command{
+ Use: "upload",
+ Short: "Uploads OS images to supported CSPs",
+ Long: "Uploads OS images to supported CSPs.",
+ PersistentPreRun: preRunRoot,
+ }
+
+ rootCmd.SetOut(os.Stdout)
+
+ rootCmd.PersistentFlags().String("raw-image", "", "Path to os image in CSP specific format that should be uploaded.")
+ rootCmd.PersistentFlags().String("pki", "", "Base path to the PKI (secure boot signing) files.")
+ rootCmd.PersistentFlags().String("variant", "", "Variant of the image being uploaded.")
+ rootCmd.PersistentFlags().String("version", "", "Shortname of the os image version.")
+ rootCmd.PersistentFlags().String("timestamp", "", "Optional timestamp to use for resource names. Uses format 2006-01-02T15:04:05Z07:00.")
+ rootCmd.PersistentFlags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
+ rootCmd.PersistentFlags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
+ rootCmd.PersistentFlags().String("out", "", "Optional path to write the upload result to. If not set, the result is written to stdout.")
+ rootCmd.PersistentFlags().Bool("verbose", false, "Enable verbose output")
+ must(rootCmd.MarkPersistentFlagRequired("raw-image"))
+ must(rootCmd.MarkPersistentFlagRequired("variant"))
+ must(rootCmd.MarkPersistentFlagRequired("version"))
+
+ rootCmd.AddCommand(cmd.NewAWSCmd())
+ rootCmd.AddCommand(cmd.NewAzureCmd())
+ rootCmd.AddCommand(cmd.NewGCPCommand())
+ rootCmd.AddCommand(cmd.NewOpenStackCmd())
+ rootCmd.AddCommand(cmd.NewQEMUCmd())
+
+ return rootCmd
+}
+
+// signalContext returns a context that is canceled on the handed signal.
+// The signal isn't watched after its first occurrence. Call the cancel
+// function to ensure the internal goroutine is stopped and the signal isn't
+// watched any longer.
+func signalContext(ctx context.Context, sig os.Signal) (context.Context, context.CancelFunc) {
+ sigCtx, stop := signal.NotifyContext(ctx, sig)
+ done := make(chan struct{}, 1)
+ stopDone := make(chan struct{}, 1)
+
+ go func() {
+ defer func() { stopDone <- struct{}{} }()
+ defer stop()
+ select {
+ case <-sigCtx.Done():
+ fmt.Println(" Signal caught. Press ctrl+c again to terminate the program immediately.")
+ case <-done:
+ }
+ }()
+
+ cancelFunc := func() {
+ done <- struct{}{}
+ <-stopDone
+ }
+
+ return sigCtx, cancelFunc
+}
+
+func preRunRoot(cmd *cobra.Command, _ []string) {
+ cmd.SilenceUsage = true
+}
+
+func must(err error) {
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/kms/config/config.go b/internal/kms/config/config.go
index 9dcaa5918..5af6d3e39 100644
--- a/internal/kms/config/config.go
+++ b/internal/kms/config/config.go
@@ -19,10 +19,14 @@ var (
"component": "constellation-kek",
}
// StorageTags are the default tags for kms client created storage solutions.
- StorageTags = map[string]string{
- "createdBy": "constellation-kms-client",
- "component": "constellation-dek-store",
+ StorageTags = map[string]*string{
+ "createdBy": toPtr("constellation-kms-client"),
+ "component": toPtr("constellation-dek-store"),
}
// AWSS3Tag is the default tag string for kms client created AWS S3 storage solutions.
AWSS3Tag = "createdBy=constellation-kms-client&component=constellation-dek-store"
)
+
+func toPtr[T any](v T) *T {
+ return &v
+}
diff --git a/internal/osimage/BUILD.bazel b/internal/osimage/BUILD.bazel
new file mode 100644
index 000000000..111c5884b
--- /dev/null
+++ b/internal/osimage/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "osimage",
+ srcs = ["osimage.go"],
+ importpath = "github.com/edgelesssys/constellation/v2/internal/osimage",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//internal/cloud/cloudprovider",
+ "//internal/osimage/secureboot",
+ "//internal/versionsapi",
+ ],
+)
diff --git a/internal/osimage/archive/BUILD.bazel b/internal/osimage/archive/BUILD.bazel
new file mode 100644
index 000000000..2a23a3efb
--- /dev/null
+++ b/internal/osimage/archive/BUILD.bazel
@@ -0,0 +1,16 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "archive",
+ srcs = ["archive.go"],
+ importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/archive",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//internal/logger",
+ "//internal/versionsapi",
+ "@com_github_aws_aws_sdk_go_v2_config//:config",
+ "@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
+ "@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
+ "@com_github_aws_aws_sdk_go_v2_service_s3//types",
+ ],
+)
diff --git a/internal/osimage/archive/archive.go b/internal/osimage/archive/archive.go
new file mode 100644
index 000000000..3937e61fc
--- /dev/null
+++ b/internal/osimage/archive/archive.go
@@ -0,0 +1,68 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+// package archive is used to archive OS images in S3.
+package archive
+
+import (
+ "context"
+ "io"
+ "net/url"
+
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/versionsapi"
+)
+
+// Archivist uploads OS images to S3.
+type Archivist struct {
+ uploadClient uploadClient
+ // bucket is the name of the S3 bucket to use.
+ bucket string
+
+ log *logger.Logger
+}
+
+// New creates a new Archivist.
+func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Archivist, error) {
+ cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
+ if err != nil {
+ return nil, err
+ }
+ s3client := s3.NewFromConfig(cfg)
+ uploadClient := s3manager.NewUploader(s3client)
+
+ return &Archivist{
+ uploadClient: uploadClient,
+ bucket: bucket,
+ log: log,
+ }, nil
+}
+
+// Archive reads the OS image in img and uploads it as key.
+func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, csp, variant string, img io.Reader) (string, error) {
+ key, err := url.JoinPath(version.ArtifactPath(), version.Kind.String(), "csp", csp, variant, "image.raw")
+ if err != nil {
+ return "", err
+ }
+ a.log.Debugf("Archiving OS image %s %s %v to s3://%v/%v", csp, variant, version.ShortPath(), a.bucket, key)
+ _, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{
+ Bucket: &a.bucket,
+ Key: &key,
+ Body: img,
+ ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
+ })
+ return baseURL + key, err
+}
+
+type uploadClient interface {
+ Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
+}
+
+const baseURL = "https://cdn.confidential.cloud/"
diff --git a/internal/osimage/aws/BUILD.bazel b/internal/osimage/aws/BUILD.bazel
new file mode 100644
index 000000000..7721504b3
--- /dev/null
+++ b/internal/osimage/aws/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "aws",
+ srcs = ["awsupload.go"],
+ importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/aws",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//internal/logger",
+ "//internal/osimage",
+ "//internal/osimage/secureboot",
+ "//internal/versionsapi",
+ "@com_github_aws_aws_sdk_go_v2_config//:config",
+ "@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
+ "@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2",
+ "@com_github_aws_aws_sdk_go_v2_service_ec2//types",
+ "@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
+ "@com_github_aws_aws_sdk_go_v2_service_s3//types",
+ "@com_github_aws_smithy_go//:smithy-go",
+ ],
+)
diff --git a/internal/osimage/aws/awsupload.go b/internal/osimage/aws/awsupload.go
new file mode 100644
index 000000000..ca903c69c
--- /dev/null
+++ b/internal/osimage/aws/awsupload.go
@@ -0,0 +1,590 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+// package aws implements uploading os images to aws.
+package aws
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "time"
+
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ "github.com/aws/aws-sdk-go-v2/service/ec2"
+ ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/aws/smithy-go"
+
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
+ "github.com/edgelesssys/constellation/v2/internal/versionsapi"
+)
+
+// Uploader can upload and remove os images on GCP.
+type Uploader struct {
+ region string
+ bucketName string
+ ec2 func(ctx context.Context, region string) (ec2API, error)
+ s3 func(ctx context.Context, region string) (s3API, error)
+ s3uploader func(ctx context.Context, region string) (s3UploaderAPI, error)
+
+ log *logger.Logger
+}
+
+// New creates a new Uploader.
+func New(region, bucketName string, log *logger.Logger) (*Uploader, error) {
+ return &Uploader{
+ region: region,
+ bucketName: bucketName,
+ ec2: func(ctx context.Context, region string) (ec2API, error) {
+ cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
+ if err != nil {
+ return nil, err
+ }
+ return ec2.NewFromConfig(cfg), nil
+ },
+ s3: func(ctx context.Context, region string) (s3API, error) {
+ cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
+ if err != nil {
+ return nil, err
+ }
+ return s3.NewFromConfig(cfg), nil
+ },
+ s3uploader: func(ctx context.Context, region string) (s3UploaderAPI, error) {
+ cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
+ if err != nil {
+ return nil, err
+ }
+ return s3manager.NewUploader(s3.NewFromConfig(cfg)), nil
+ },
+
+ log: log,
+ }, nil
+}
+
+// Upload uploads an OS image to AWS.
+func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) (map[string]string, error) {
+ blobName := fmt.Sprintf("image-%s-%s-%d.raw", req.Version.Stream, req.Version.Version, req.Timestamp.Unix())
+ imageName := imageName(req.Version, req.Timestamp)
+ allRegions := []string{u.region}
+ allRegions = append(allRegions, replicationRegions...)
+ // TODO(malt3): make this configurable
+ publish := true
+ amiIDs := make(map[string]string, len(allRegions))
+ if err := u.ensureBucket(ctx); err != nil {
+ return nil, fmt.Errorf("ensuring bucket %s exists: %w", u.bucketName, err)
+ }
+
+ // pre-cleaning
+ for _, region := range allRegions {
+ if err := u.ensureImageDeleted(ctx, imageName, region); err != nil {
+ return nil, fmt.Errorf("pre-cleaning: ensuring no image under the name %s in region %s: %w", imageName, region, err)
+ }
+ }
+ if err := u.ensureSnapshotDeleted(ctx, imageName, u.region); err != nil {
+ return nil, fmt.Errorf("pre-cleaning: ensuring no snapshot using the same name exists: %w", err)
+ }
+ if err := u.ensureBlobDeleted(ctx, blobName); err != nil {
+ return nil, fmt.Errorf("pre-cleaning: ensuring no blob using the same name exists: %w", err)
+ }
+
+ // create primary image
+ if err := u.uploadBlob(ctx, blobName, req.Image); err != nil {
+ return nil, fmt.Errorf("uploading image to s3: %w", err)
+ }
+ defer func() {
+ if err := u.ensureBlobDeleted(ctx, blobName); err != nil {
+ u.log.Errorf("post-cleaning: deleting temporary blob from s3", err)
+ }
+ }()
+ snapshotID, err := u.importSnapshot(ctx, blobName, imageName)
+ if err != nil {
+ return nil, fmt.Errorf("importing snapshot: %w", err)
+ }
+ primaryAMIID, err := u.createImageFromSnapshot(ctx, req.Version, imageName, snapshotID, req.UEFIVarStore)
+ if err != nil {
+ return nil, fmt.Errorf("creating image from snapshot: %w", err)
+ }
+ amiIDs[u.region] = primaryAMIID
+ if err := u.waitForImage(ctx, primaryAMIID, u.region); err != nil {
+ return nil, fmt.Errorf("waiting for primary image to become available: %w", err)
+ }
+
+ // replicate image
+ for _, region := range replicationRegions {
+ amiID, err := u.replicateImage(ctx, imageName, primaryAMIID, region)
+ if err != nil {
+ return nil, fmt.Errorf("replicating image to region %s: %w", region, err)
+ }
+ amiIDs[region] = amiID
+ }
+
+ // wait for replication, tag, publish
+ for _, region := range allRegions {
+ if err := u.waitForImage(ctx, amiIDs[region], region); err != nil {
+ return nil, fmt.Errorf("waiting for image to become available in region %s: %w", region, err)
+ }
+ if err := u.tagImageAndSnapshot(ctx, imageName, amiIDs[region], region); err != nil {
+ return nil, fmt.Errorf("tagging image in region %s: %w", region, err)
+ }
+ if !publish {
+ continue
+ }
+ if err := u.publishImage(ctx, amiIDs[region], region); err != nil {
+ return nil, fmt.Errorf("publishing image in region %s: %w", region, err)
+ }
+ }
+ return amiIDs, nil
+}
+
+func (u *Uploader) ensureBucket(ctx context.Context) error {
+ s3C, err := u.s3(ctx, u.region)
+ if err != nil {
+ return fmt.Errorf("determining if bucket %s exists: %w", u.bucketName, err)
+ }
+ _, err = s3C.HeadBucket(ctx, &s3.HeadBucketInput{
+ Bucket: &u.bucketName,
+ })
+ if err == nil {
+ u.log.Debugf("Bucket %s exists", u.bucketName)
+ return nil
+ }
+ var noSuchBucketErr *types.NoSuchBucket
+ if !errors.As(err, &noSuchBucketErr) {
+ return fmt.Errorf("determining if bucket %s exists: %w", u.bucketName, err)
+ }
+ u.log.Debugf("Creating bucket %s", u.bucketName)
+ _, err = s3C.CreateBucket(ctx, &s3.CreateBucketInput{
+ Bucket: &u.bucketName,
+ })
+ if err != nil {
+ return fmt.Errorf("creating bucket %s: %w", u.bucketName, err)
+ }
+ return nil
+}
+
+func (u *Uploader) uploadBlob(ctx context.Context, blobName string, img io.Reader) error {
+ u.log.Debugf("Uploading os image as %s", blobName)
+ uploadC, err := u.s3uploader(ctx, u.region)
+ if err != nil {
+ return err
+ }
+ _, err = uploadC.Upload(ctx, &s3.PutObjectInput{
+ Bucket: &u.bucketName,
+ Key: &blobName,
+ Body: img,
+ ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
+ })
+ return err
+}
+
+func (u *Uploader) ensureBlobDeleted(ctx context.Context, blobName string) error {
+ s3C, err := u.s3(ctx, u.region)
+ if err != nil {
+ return err
+ }
+ _, err = s3C.HeadObject(ctx, &s3.HeadObjectInput{
+ Bucket: &u.bucketName,
+ Key: &blobName,
+ })
+ var apiError smithy.APIError
+ if errors.As(err, &apiError) && apiError.ErrorCode() == "NotFound" {
+ u.log.Debugf("Blob %s in %s doesn't exist. Nothing to clean up.", blobName, u.bucketName)
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ u.log.Debugf("Deleting blob %s", blobName)
+ _, err = s3C.DeleteObject(ctx, &s3.DeleteObjectInput{
+ Bucket: &u.bucketName,
+ Key: &blobName,
+ })
+ return err
+}
+
+func (u *Uploader) findSnapshots(ctx context.Context, snapshotName, region string) ([]string, error) {
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return nil, fmt.Errorf("creating ec2 client: %w", err)
+ }
+ snapshots, err := ec2C.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{
+ Filters: []ec2types.Filter{
+ {
+ Name: toPtr("tag:Name"),
+ Values: []string{snapshotName},
+ },
+ },
+ })
+ if err != nil {
+ return nil, fmt.Errorf("describing snapshots: %w", err)
+ }
+ var snapshotIDs []string
+ for _, s := range snapshots.Snapshots {
+ if s.SnapshotId == nil {
+ continue
+ }
+ snapshotIDs = append(snapshotIDs, *s.SnapshotId)
+ }
+ return snapshotIDs, nil
+}
+
+func (u *Uploader) importSnapshot(ctx context.Context, blobName, snapshotName string) (string, error) {
+ u.log.Debugf("Importing %s as snapshot %s", blobName, snapshotName)
+ ec2C, err := u.ec2(ctx, u.region)
+ if err != nil {
+ return "", fmt.Errorf("creating ec2 client: %w", err)
+ }
+ importResp, err := ec2C.ImportSnapshot(ctx, &ec2.ImportSnapshotInput{
+ ClientData: &ec2types.ClientData{
+ Comment: &snapshotName,
+ },
+ Description: &snapshotName,
+ DiskContainer: &ec2types.SnapshotDiskContainer{
+ Description: &snapshotName,
+ Format: toPtr(string(ec2types.DiskImageFormatRaw)),
+ UserBucket: &ec2types.UserBucket{
+ S3Bucket: &u.bucketName,
+ S3Key: &blobName,
+ },
+ },
+ })
+ if err != nil {
+ return "", fmt.Errorf("importing snapshot: %w", err)
+ }
+ if importResp.ImportTaskId == nil {
+ return "", fmt.Errorf("importing snapshot: no import task ID returned")
+ }
+ u.log.Debugf("Waiting for snapshot %s to be ready", snapshotName)
+ return waitForSnapshotImport(ctx, ec2C, *importResp.ImportTaskId)
+}
+
+func (u *Uploader) ensureSnapshotDeleted(ctx context.Context, snapshotName, region string) error {
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return fmt.Errorf("creating ec2 client: %w", err)
+ }
+ snapshots, err := u.findSnapshots(ctx, snapshotName, region)
+ if err != nil {
+ return fmt.Errorf("finding snapshots: %w", err)
+ }
+ for _, snapshot := range snapshots {
+ u.log.Debugf("Deleting snapshot %s in %s", snapshot, region)
+ _, err = ec2C.DeleteSnapshot(ctx, &ec2.DeleteSnapshotInput{
+ SnapshotId: toPtr(snapshot),
+ })
+ if err != nil {
+ return fmt.Errorf("deleting snapshot %s: %w", snapshot, err)
+ }
+ }
+ return nil
+}
+
+func (u *Uploader) createImageFromSnapshot(ctx context.Context, version versionsapi.Version, imageName, snapshotID string, uefiVarStore secureboot.UEFIVarStore) (string, error) {
+ u.log.Debugf("Creating image %s in %s", imageName, u.region)
+ ec2C, err := u.ec2(ctx, u.region)
+ if err != nil {
+ return "", fmt.Errorf("creating ec2 client: %w", err)
+ }
+ uefiData, err := uefiVarStore.ToAWS()
+ if err != nil {
+ return "", fmt.Errorf("creating uefi data: %w", err)
+ }
+ createReq, err := ec2C.RegisterImage(ctx, &ec2.RegisterImageInput{
+ Name: &imageName,
+ Architecture: ec2types.ArchitectureValuesX8664,
+ BlockDeviceMappings: []ec2types.BlockDeviceMapping{
+ {
+ DeviceName: toPtr("/dev/xvda"),
+ Ebs: &ec2types.EbsBlockDevice{
+ DeleteOnTermination: toPtr(true),
+ SnapshotId: &snapshotID,
+ },
+ },
+ },
+ BootMode: ec2types.BootModeValuesUefi,
+ Description: toPtr("Constellation " + version.ShortPath()),
+ EnaSupport: toPtr(true),
+ RootDeviceName: toPtr("/dev/xvda"),
+ TpmSupport: ec2types.TpmSupportValuesV20,
+ UefiData: &uefiData,
+ VirtualizationType: toPtr("hvm"),
+ })
+ if err != nil {
+ return "", fmt.Errorf("creating image: %w", err)
+ }
+ if createReq.ImageId == nil {
+ return "", fmt.Errorf("creating image: no image ID returned")
+ }
+ return *createReq.ImageId, nil
+}
+
+func (u *Uploader) replicateImage(ctx context.Context, imageName, amiID string, region string) (string, error) {
+ u.log.Debugf("Replicating image %s to %s", imageName, region)
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return "", fmt.Errorf("creating ec2 client: %w", err)
+ }
+ replicateReq, err := ec2C.CopyImage(ctx, &ec2.CopyImageInput{
+ Name: &imageName,
+ SourceImageId: &amiID,
+ SourceRegion: &u.region,
+ })
+ if err != nil {
+ return "", fmt.Errorf("replicating image: %w", err)
+ }
+ if replicateReq.ImageId == nil {
+ return "", fmt.Errorf("replicating image: no image ID returned")
+ }
+ return *replicateReq.ImageId, nil
+}
+
+func (u *Uploader) findImage(ctx context.Context, imageName, region string) (string, error) {
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return "", fmt.Errorf("creating ec2 client: %w", err)
+ }
+ snapshots, err := ec2C.DescribeImages(ctx, &ec2.DescribeImagesInput{
+ Filters: []ec2types.Filter{
+ {
+ Name: toPtr("name"),
+ Values: []string{imageName},
+ },
+ },
+ })
+ if err != nil {
+ return "", fmt.Errorf("describing images: %w", err)
+ }
+ if len(snapshots.Images) == 0 {
+ return "", errAMIDoesNotExist
+ }
+ if len(snapshots.Images) != 1 {
+ return "", fmt.Errorf("expected 1 image, got %d", len(snapshots.Images))
+ }
+ if snapshots.Images[0].ImageId == nil {
+ return "", fmt.Errorf("image ID is nil")
+ }
+ return *snapshots.Images[0].ImageId, nil
+}
+
+func (u *Uploader) waitForImage(ctx context.Context, amiID, region string) error {
+ u.log.Debugf("Waiting for image %s in %s to be created", amiID, region)
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return fmt.Errorf("creating ec2 client: %w", err)
+ }
+ waiter := ec2.NewImageAvailableWaiter(ec2C)
+ err = waiter.Wait(ctx, &ec2.DescribeImagesInput{
+ ImageIds: []string{amiID},
+ }, maxWait)
+ if err != nil {
+ return fmt.Errorf("waiting for image: %w", err)
+ }
+ return nil
+}
+
+func (u *Uploader) tagImageAndSnapshot(ctx context.Context, imageName, amiID, region string) error {
+ u.log.Debugf("Tagging backing snapshot of image %s in %s", amiID, region)
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return fmt.Errorf("creating ec2 client: %w", err)
+ }
+ snapshotID, err := getBackingSnapshotID(ctx, ec2C, amiID)
+ if err != nil {
+ return fmt.Errorf("getting backing snapshot ID: %w", err)
+ }
+ _, err = ec2C.CreateTags(ctx, &ec2.CreateTagsInput{
+ Resources: []string{amiID, snapshotID},
+ Tags: []ec2types.Tag{
+ {
+ Key: toPtr("Name"),
+ Value: toPtr(imageName),
+ },
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("tagging ami and snapshot: %w", err)
+ }
+ return nil
+}
+
+func (u *Uploader) publishImage(ctx context.Context, imageName, region string) error {
+ u.log.Debugf("Publishing image %s in %s", imageName, region)
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return fmt.Errorf("creating ec2 client: %w", err)
+ }
+ _, err = ec2C.ModifyImageAttribute(ctx, &ec2.ModifyImageAttributeInput{
+ ImageId: &imageName,
+ LaunchPermission: &ec2types.LaunchPermissionModifications{
+ Add: []ec2types.LaunchPermission{
+ {
+ Group: ec2types.PermissionGroupAll,
+ },
+ },
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("publishing image: %w", err)
+ }
+ return nil
+}
+
+func (u *Uploader) ensureImageDeleted(ctx context.Context, imageName, region string) error {
+ ec2C, err := u.ec2(ctx, region)
+ if err != nil {
+ return fmt.Errorf("creating ec2 client: %w", err)
+ }
+ amiID, err := u.findImage(ctx, imageName, region)
+ if err == errAMIDoesNotExist {
+ u.log.Debugf("Image %s in %s doesn't exist. Nothing to clean up.", imageName, region)
+ return nil
+ }
+ snapshotID, err := getBackingSnapshotID(ctx, ec2C, amiID)
+ if err == errAMIDoesNotExist {
+ u.log.Debugf("Image %s doesn't exist. Nothing to clean up.", amiID)
+ return nil
+ }
+ u.log.Debugf("Deleting image %s in %s with backing snapshot", amiID, region)
+ _, err = ec2C.DeregisterImage(ctx, &ec2.DeregisterImageInput{
+ ImageId: &amiID,
+ })
+ if err != nil {
+ return fmt.Errorf("deleting image: %w", err)
+ }
+ _, err = ec2C.DeleteSnapshot(ctx, &ec2.DeleteSnapshotInput{
+ SnapshotId: &snapshotID,
+ })
+ if err != nil {
+ return fmt.Errorf("deleting snapshot: %w", err)
+ }
+ return nil
+}
+
+func imageName(version versionsapi.Version, timestamp time.Time) string {
+ if version.Stream == "stable" {
+ return fmt.Sprintf("constellation-%s", version.Version)
+ }
+ return fmt.Sprintf("constellation-%s-%s-%s", version.Stream, version.Version, timestamp.Format(timestampFormat))
+}
+
+func waitForSnapshotImport(ctx context.Context, ec2C ec2API, importTaskID string) (string, error) {
+ for {
+ taskResp, err := ec2C.DescribeImportSnapshotTasks(ctx, &ec2.DescribeImportSnapshotTasksInput{
+ ImportTaskIds: []string{importTaskID},
+ })
+ if err != nil {
+ return "", fmt.Errorf("describing import snapshot task: %w", err)
+ }
+ if len(taskResp.ImportSnapshotTasks) == 0 {
+ return "", fmt.Errorf("describing import snapshot task: no tasks returned")
+ }
+ if taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail == nil {
+ return "", fmt.Errorf("describing import snapshot task: no snapshot task detail returned")
+ }
+ if taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail.Status == nil {
+ return "", fmt.Errorf("describing import snapshot task: no status returned")
+ }
+ switch *taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail.Status {
+ case string(ec2types.SnapshotStateCompleted):
+ return *taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId, nil
+ case string(ec2types.SnapshotStateError):
+ return "", fmt.Errorf("importing snapshot: task failed")
+ }
+ time.Sleep(waitInterval)
+ }
+}
+
+func getBackingSnapshotID(ctx context.Context, ec2C ec2API, amiID string) (string, error) {
+ describeResp, err := ec2C.DescribeImages(ctx, &ec2.DescribeImagesInput{
+ ImageIds: []string{amiID},
+ })
+ if err != nil || len(describeResp.Images) == 0 {
+ return "", errAMIDoesNotExist
+ }
+ if len(describeResp.Images) != 1 {
+ return "", fmt.Errorf("describing image: expected 1 image, got %d", len(describeResp.Images))
+ }
+ image := describeResp.Images[0]
+ if len(image.BlockDeviceMappings) != 1 {
+ return "", fmt.Errorf("found %d block device mappings for image %s, expected 1", len(image.BlockDeviceMappings), amiID)
+ }
+ if image.BlockDeviceMappings[0].Ebs == nil {
+ return "", fmt.Errorf("image %s does not have an EBS block device mapping", amiID)
+ }
+ ebs := image.BlockDeviceMappings[0].Ebs
+ if ebs.SnapshotId == nil {
+ return "", fmt.Errorf("image %s does not have an EBS snapshot", amiID)
+ }
+ return *ebs.SnapshotId, nil
+}
+
+type ec2API interface {
+ DescribeImages(ctx context.Context, params *ec2.DescribeImagesInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.DescribeImagesOutput, error)
+ ModifyImageAttribute(ctx context.Context, params *ec2.ModifyImageAttributeInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.ModifyImageAttributeOutput, error)
+ RegisterImage(ctx context.Context, params *ec2.RegisterImageInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.RegisterImageOutput, error)
+ CopyImage(ctx context.Context, params *ec2.CopyImageInput, optFns ...func(*ec2.Options),
+ ) (*ec2.CopyImageOutput, error)
+ DeregisterImage(ctx context.Context, params *ec2.DeregisterImageInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.DeregisterImageOutput, error)
+ ImportSnapshot(ctx context.Context, params *ec2.ImportSnapshotInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.ImportSnapshotOutput, error)
+ DescribeImportSnapshotTasks(ctx context.Context, params *ec2.DescribeImportSnapshotTasksInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.DescribeImportSnapshotTasksOutput, error)
+ DescribeSnapshots(ctx context.Context, params *ec2.DescribeSnapshotsInput,
+ optFns ...func(*ec2.Options),
+ ) (*ec2.DescribeSnapshotsOutput, error)
+ DeleteSnapshot(ctx context.Context, params *ec2.DeleteSnapshotInput, optFns ...func(*ec2.Options),
+ ) (*ec2.DeleteSnapshotOutput, error)
+ CreateTags(ctx context.Context, params *ec2.CreateTagsInput, optFns ...func(*ec2.Options),
+ ) (*ec2.CreateTagsOutput, error)
+}
+
+type s3API interface {
+ HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options),
+ ) (*s3.HeadBucketOutput, error)
+ CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options),
+ ) (*s3.CreateBucketOutput, error)
+ HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options),
+ ) (*s3.HeadObjectOutput, error)
+ DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options),
+ ) (*s3.DeleteObjectOutput, error)
+}
+
+type s3UploaderAPI interface {
+ Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader),
+ ) (*s3manager.UploadOutput, error)
+}
+
+func toPtr[T any](v T) *T {
+ return &v
+}
+
+const (
+ waitInterval = 15 * time.Second
+ maxWait = 15 * time.Minute
+ timestampFormat = "20060102150405"
+)
+
+var (
+ errAMIDoesNotExist = errors.New("ami does not exist")
+ replicationRegions = []string{"us-east-2", "ap-south-1"}
+)
diff --git a/internal/osimage/azure/BUILD.bazel b/internal/osimage/azure/BUILD.bazel
new file mode 100644
index 000000000..86dec0251
--- /dev/null
+++ b/internal/osimage/azure/BUILD.bazel
@@ -0,0 +1,21 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "azure",
+ srcs = [
+ "azureupload.go",
+ "disktype_string.go",
+ ],
+ importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/azure",
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//internal/logger",
+ "//internal/osimage",
+ "//internal/versionsapi",
+ "@com_github_azure_azure_sdk_for_go_sdk_azcore//runtime",
+ "@com_github_azure_azure_sdk_for_go_sdk_azidentity//:azidentity",
+ "@com_github_azure_azure_sdk_for_go_sdk_resourcemanager_compute_armcompute_v4//:armcompute",
+ "@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//blob",
+ "@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//pageblob",
+ ],
+)
diff --git a/internal/osimage/azure/azureupload.go b/internal/osimage/azure/azureupload.go
new file mode 100644
index 000000000..bc8154ada
--- /dev/null
+++ b/internal/osimage/azure/azureupload.go
@@ -0,0 +1,701 @@
+/*
+Copyright (c) Edgeless Systems GmbH
+
+SPDX-License-Identifier: AGPL-3.0-only
+*/
+
+// package azure implements uploading os images to azure.
+package azure
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+ armcomputev4 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4"
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
+ "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/pageblob"
+ "github.com/edgelesssys/constellation/v2/internal/logger"
+ "github.com/edgelesssys/constellation/v2/internal/osimage"
+ "github.com/edgelesssys/constellation/v2/internal/versionsapi"
+)
+
+// Uploader can upload and remove os images on Azure.
+type Uploader struct {
+ subscription string
+ location string
+ resourceGroup string
+ pollingFrequency time.Duration
+ disks azureDiskAPI
+ managedImages azureManagedImageAPI
+ blob sasBlobUploader
+ galleries azureGalleriesAPI
+ image azureGalleriesImageAPI
+ imageVersions azureGalleriesImageVersionAPI
+ communityVersions azureCommunityGalleryImageVersionAPI
+
+ log *logger.Logger
+}
+
+// New creates a new Uploader.
+func New(subscription, location, resourceGroup string, log *logger.Logger) (*Uploader, error) {
+ cred, err := azidentity.NewDefaultAzureCredential(nil)
+ if err != nil {
+ return nil, err
+ }
+ diskClient, err := armcomputev4.NewDisksClient(subscription, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+ managedImagesClient, err := armcomputev4.NewImagesClient(subscription, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+ galleriesClient, err := armcomputev4.NewGalleriesClient(subscription, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+ galleriesImageClient, err := armcomputev4.NewGalleryImagesClient(subscription, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+ galleriesImageVersionClient, err := armcomputev4.NewGalleryImageVersionsClient(subscription, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+ communityImageVersionClient, err := armcomputev4.NewCommunityGalleryImageVersionsClient(subscription, cred, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Uploader{
+ subscription: subscription,
+ location: location,
+ resourceGroup: resourceGroup,
+ pollingFrequency: pollingFrequency,
+ disks: diskClient,
+ managedImages: managedImagesClient,
+ blob: func(sasBlobURL string) (azurePageblobAPI, error) {
+ return pageblob.NewClientWithNoCredential(sasBlobURL, nil)
+ },
+ galleries: galleriesClient,
+ image: galleriesImageClient,
+ imageVersions: galleriesImageVersionClient,
+ communityVersions: communityImageVersionClient,
+ log: log,
+ }, nil
+}
+
+// Upload uploads an OS image to Azure.
+func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) (map[string]string, error) {
+ formattedTime := req.Timestamp.Format(timestampFormat)
+ diskName := fmt.Sprintf("constellation-%s-%s-%s", req.Version.Stream, formattedTime, req.Variant)
+ var sigName string
+ switch req.Version.Stream {
+ case "stable":
+ sigName = sigNameStable
+ case "debug":
+ sigName = sigNameDebug
+ default:
+ sigName = sigNameDefault
+ }
+ definitionName := imageOffer(req.Version)
+ versionName, err := imageVersion(req.Version, req.Timestamp)
+ if err != nil {
+ return nil, fmt.Errorf("determining image version name: %w", err)
+ }
+
+ // ensure new image can be uploaded by deleting existing resources using the same name
+ if err := u.ensureImageVersionDeleted(ctx, sigName, definitionName, versionName); err != nil {
+ return nil, fmt.Errorf("pre-cleaning: ensuring no image version using the same name exists: %w", err)
+ }
+ if err := u.ensureManagedImageDeleted(ctx, diskName); err != nil {
+ return nil, fmt.Errorf("pre-cleaning: ensuring no managed image using the same name exists: %w", err)
+ }
+ if err := u.ensureDiskDeleted(ctx, diskName); err != nil {
+ return nil, fmt.Errorf("pre-cleaning: ensuring no temporary disk using the same name exists: %w", err)
+ }
+
+ diskID, err := u.createDisk(ctx, diskName, DiskTypeNormal, req.Image, nil, req.Size)
+ if err != nil {
+ return nil, fmt.Errorf("creating disk: %w", err)
+ }
+ defer func() {
+ // cleanup temp disk
+ err := u.ensureDiskDeleted(ctx, diskName)
+ if err != nil {
+ u.log.Errorf("post-cleaning: deleting disk image: %v", err)
+ }
+ }()
+ managedImageID, err := u.createManagedImage(ctx, diskName, diskID)
+ if err != nil {
+ return nil, fmt.Errorf("creating managed image: %w", err)
+ }
+ if err := u.ensureSIG(ctx, sigName); err != nil {
+ return nil, fmt.Errorf("ensuring sig exists: %w", err)
+ }
+ if err := u.ensureImageDefinition(ctx, sigName, definitionName, req.Version, req.Variant); err != nil {
+ return nil, fmt.Errorf("ensuring image definition exists: %w", err)
+ }
+
+ unsharedImageVersionID, err := u.createImageVersion(ctx, sigName, definitionName, versionName, managedImageID)
+ if err != nil {
+ return nil, fmt.Errorf("creating image version: %w", err)
+ }
+
+ imageReference, err := u.getImageReference(ctx, sigName, definitionName, versionName, unsharedImageVersionID)
+ if err != nil {
+ return nil, fmt.Errorf("getting image reference: %w", err)
+ }
+
+ return map[string]string{
+ req.Variant: imageReference,
+ }, nil
+}
+
+// createDisk creates and initializes (uploads contents of) an azure disk.
+func (u *Uploader) createDisk(ctx context.Context, diskName string, diskType DiskType, img io.ReadSeeker, vmgs io.ReadSeeker, size int64) (string, error) {
+ u.log.Debugf("Creating disk %s in %s", diskName, u.resourceGroup)
+ if diskType == DiskTypeWithVMGS && vmgs == nil {
+ return "", errors.New("cannot create disk with vmgs: vmgs reader is nil")
+ }
+ var createOption armcomputev4.DiskCreateOption
+ var requestVMGSSAS bool
+ switch diskType {
+ case DiskTypeNormal:
+ createOption = armcomputev4.DiskCreateOptionUpload
+ case DiskTypeWithVMGS:
+ createOption = armcomputev4.DiskCreateOptionUploadPreparedSecure
+ requestVMGSSAS = true
+ }
+ disk := armcomputev4.Disk{
+ Location: &u.location,
+ Properties: &armcomputev4.DiskProperties{
+ CreationData: &armcomputev4.CreationData{
+ CreateOption: &createOption,
+ UploadSizeBytes: toPtr(size),
+ },
+ HyperVGeneration: toPtr(armcomputev4.HyperVGenerationV2),
+ OSType: toPtr(armcomputev4.OperatingSystemTypesLinux),
+ },
+ }
+ createPoller, err := u.disks.BeginCreateOrUpdate(ctx, u.resourceGroup, diskName, disk, &armcomputev4.DisksClientBeginCreateOrUpdateOptions{})
+ if err != nil {
+ return "", fmt.Errorf("creating disk: %w", err)
+ }
+ createdDisk, err := createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
+ if err != nil {
+ return "", fmt.Errorf("waiting for disk to be created: %w", err)
+ }
+
+ u.log.Debugf("Granting temporary upload permissions via SAS token")
+ accessGrant := armcomputev4.GrantAccessData{
+ Access: toPtr(armcomputev4.AccessLevelWrite),
+ DurationInSeconds: toPtr(int32(uploadAccessDuration)),
+ GetSecureVMGuestStateSAS: &requestVMGSSAS,
+ }
+ accessPoller, err := u.disks.BeginGrantAccess(ctx, u.resourceGroup, diskName, accessGrant, &armcomputev4.DisksClientBeginGrantAccessOptions{})
+ if err != nil {
+ return "", fmt.Errorf("generating disk sas token: %w", err)
+ }
+ accesPollerResp, err := accessPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
+ if err != nil {
+ return "", fmt.Errorf("waiting for sas token: %w", err)
+ }
+
+ if requestVMGSSAS {
+ u.log.Debugf("Uploading vmgs")
+ vmgsSize, err := vmgs.Seek(0, io.SeekEnd)
+ if err != nil {
+ return "", err
+ }
+ if _, err := vmgs.Seek(0, io.SeekStart); err != nil {
+ return "", err
+ }
+ if accesPollerResp.SecurityDataAccessSAS == nil {
+ return "", errors.New("uploading vmgs: grant access returned no vmgs sas")
+ }
+ if err := uploadBlob(ctx, *accesPollerResp.SecurityDataAccessSAS, vmgs, vmgsSize, u.blob); err != nil {
+ return "", fmt.Errorf("uploading vmgs: %w", err)
+ }
+ }
+ u.log.Debugf("Uploading os image")
+ if accesPollerResp.AccessSAS == nil {
+ return "", errors.New("uploading disk: grant access returned no disk sas")
+ }
+ if err := uploadBlob(ctx, *accesPollerResp.AccessSAS, img, size, u.blob); err != nil {
+ return "", fmt.Errorf("uploading image: %w", err)
+ }
+ revokePoller, err := u.disks.BeginRevokeAccess(ctx, u.resourceGroup, diskName, &armcomputev4.DisksClientBeginRevokeAccessOptions{})
+ if err != nil {
+ return "", fmt.Errorf("revoking disk sas token: %w", err)
+ }
+ if _, err := revokePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
+ return "", fmt.Errorf("waiting for sas token revocation: %w", err)
+ }
+ if createdDisk.ID == nil {
+ return "", errors.New("created disk has no id")
+ }
+ return *createdDisk.ID, nil
+}
+
+func (u *Uploader) ensureDiskDeleted(ctx context.Context, diskName string) error {
+ _, err := u.disks.Get(ctx, u.resourceGroup, diskName, &armcomputev4.DisksClientGetOptions{})
+ if err != nil {
+ u.log.Debugf("Disk %s in %s doesn't exist. Nothing to clean up.", diskName, u.resourceGroup)
+ return nil
+ }
+ u.log.Debugf("Deleting disk %s in %s", diskName, u.resourceGroup)
+ deletePoller, err := u.disks.BeginDelete(ctx, u.resourceGroup, diskName, &armcomputev4.DisksClientBeginDeleteOptions{})
+ if err != nil {
+ return fmt.Errorf("deleting disk: %w", err)
+ }
+ if _, err = deletePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
+ return fmt.Errorf("waiting for disk to be deleted: %w", err)
+ }
+ return nil
+}
+
+func (u *Uploader) createManagedImage(ctx context.Context, imageName string, diskID string) (string, error) {
+ u.log.Debugf("Creating managed image %s in %s", imageName, u.resourceGroup)
+ image := armcomputev4.Image{
+ Location: &u.location,
+ Properties: &armcomputev4.ImageProperties{
+ HyperVGeneration: toPtr(armcomputev4.HyperVGenerationTypesV2),
+ StorageProfile: &armcomputev4.ImageStorageProfile{
+ OSDisk: &armcomputev4.ImageOSDisk{
+ OSState: toPtr(armcomputev4.OperatingSystemStateTypesGeneralized),
+ OSType: toPtr(armcomputev4.OperatingSystemTypesLinux),
+ ManagedDisk: &armcomputev4.SubResource{
+ ID: &diskID,
+ },
+ },
+ },
+ },
+ }
+ createPoller, err := u.managedImages.BeginCreateOrUpdate(
+ ctx, u.resourceGroup, imageName, image,
+ &armcomputev4.ImagesClientBeginCreateOrUpdateOptions{},
+ )
+ if err != nil {
+ return "", fmt.Errorf("creating managed image: %w", err)
+ }
+ createdImage, err := createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
+ if err != nil {
+ return "", fmt.Errorf("waiting for image to be created: %w", err)
+ }
+ if createdImage.ID == nil {
+ return "", errors.New("created image has no id")
+ }
+ return *createdImage.ID, nil
+}
+
+func (u *Uploader) ensureManagedImageDeleted(ctx context.Context, imageName string) error {
+ _, err := u.managedImages.Get(ctx, u.resourceGroup, imageName, &armcomputev4.ImagesClientGetOptions{})
+ if err != nil {
+ u.log.Debugf("Managed image %s in %s doesn't exist. Nothing to clean up.", imageName, u.resourceGroup)
+ return nil
+ }
+ u.log.Debugf("Deleting managed image %s in %s", imageName, u.resourceGroup)
+ deletePoller, err := u.managedImages.BeginDelete(ctx, u.resourceGroup, imageName, &armcomputev4.ImagesClientBeginDeleteOptions{})
+ if err != nil {
+ return fmt.Errorf("deleting image: %w", err)
+ }
+ if _, err = deletePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
+ return fmt.Errorf("waiting for image to be deleted: %w", err)
+ }
+ return nil
+}
+
+// ensureSIG creates a SIG if it does not exist yet.
+func (u *Uploader) ensureSIG(ctx context.Context, sigName string) error {
+ _, err := u.galleries.Get(ctx, u.resourceGroup, sigName, &armcomputev4.GalleriesClientGetOptions{})
+ if err == nil {
+ u.log.Debugf("Image gallery %s in %s exists", sigName, u.resourceGroup)
+ return nil
+ }
+ u.log.Debugf("Creating image gallery %s in %s", sigName, u.resourceGroup)
+ gallery := armcomputev4.Gallery{
+ Location: &u.location,
+ }
+ createPoller, err := u.galleries.BeginCreateOrUpdate(ctx, u.resourceGroup, sigName, gallery,
+ &armcomputev4.GalleriesClientBeginCreateOrUpdateOptions{},
+ )
+ if err != nil {
+ return fmt.Errorf("creating image gallery: %w", err)
+ }
+ if _, err = createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
+ return fmt.Errorf("waiting for image gallery to be created: %w", err)
+ }
+ return nil
+}
+
+// ensureImageDefinition creates an image definition (component of a SIG) if it does not exist yet.
+func (u *Uploader) ensureImageDefinition(ctx context.Context, sigName, definitionName string, version versionsapi.Version, variant string) error {
+ _, err := u.image.Get(ctx, u.resourceGroup, sigName, definitionName, &armcomputev4.GalleryImagesClientGetOptions{})
+ if err == nil {
+ u.log.Debugf("Image definition %s/%s in %s exists", sigName, definitionName, u.resourceGroup)
+ return nil
+ }
+ u.log.Debugf("Creating image definition %s/%s in %s", sigName, definitionName, u.resourceGroup)
+ var securityType string
+ // TODO(malt3): This needs to allow the *Supported or the normal variant
+ // based on wether a VMGS was provided or not.
+ // VMGS provided: ConfidentialVM
+ // No VMGS provided: ConfidentialVMSupported
+ switch strings.ToLower(variant) {
+ case "cvm":
+ securityType = string("ConfidentialVMSupported")
+ case "trustedlaunch":
+ securityType = string(armcomputev4.SecurityTypesTrustedLaunch)
+ }
+ offer := imageOffer(version)
+
+ galleryImage := armcomputev4.GalleryImage{
+ Location: &u.location,
+ Properties: &armcomputev4.GalleryImageProperties{
+ Identifier: &armcomputev4.GalleryImageIdentifier{
+ Offer: &offer,
+ Publisher: toPtr(imageDefinitionPublisher),
+ SKU: toPtr(imageDefinitionSKU),
+ },
+ OSState: toPtr(armcomputev4.OperatingSystemStateTypesGeneralized),
+ OSType: toPtr(armcomputev4.OperatingSystemTypesLinux),
+ Architecture: toPtr(armcomputev4.ArchitectureX64),
+ Features: []*armcomputev4.GalleryImageFeature{
+ {
+ Name: toPtr("SecurityType"),
+ Value: &securityType,
+ },
+ },
+ HyperVGeneration: toPtr(armcomputev4.HyperVGenerationV2),
+ },
+ }
+ createPoller, err := u.image.BeginCreateOrUpdate(ctx, u.resourceGroup, sigName, definitionName, galleryImage,
+ &armcomputev4.GalleryImagesClientBeginCreateOrUpdateOptions{},
+ )
+ if err != nil {
+ return fmt.Errorf("creating image definition: %w", err)
+ }
+ if _, err = createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
+ return fmt.Errorf("waiting for image definition to be created: %w", err)
+ }
+ return nil
+}
+
+func (u *Uploader) createImageVersion(ctx context.Context, sigName, definitionName, versionName, imageID string) (string, error) {
+ u.log.Debugf("Creating image version %s/%s/%s in %s", sigName, definitionName, versionName, u.resourceGroup)
+ imageVersion := armcomputev4.GalleryImageVersion{
+ Location: &u.location,
+ Properties: &armcomputev4.GalleryImageVersionProperties{
+ StorageProfile: &armcomputev4.GalleryImageVersionStorageProfile{
+ OSDiskImage: &armcomputev4.GalleryOSDiskImage{
+ HostCaching: toPtr(armcomputev4.HostCachingReadOnly),
+ },
+ Source: &armcomputev4.GalleryArtifactVersionFullSource{
+ ID: &imageID,
+ },
+ },
+ PublishingProfile: &armcomputev4.GalleryImageVersionPublishingProfile{
+ ReplicaCount: toPtr[int32](1),
+ ReplicationMode: toPtr(armcomputev4.ReplicationModeFull),
+ TargetRegions: targetRegions,
+ },
+ },
+ }
+ createPoller, err := u.imageVersions.BeginCreateOrUpdate(ctx, u.resourceGroup, sigName, definitionName, versionName, imageVersion,
+ &armcomputev4.GalleryImageVersionsClientBeginCreateOrUpdateOptions{},
+ )
+ if err != nil {
+ return "", fmt.Errorf("creating image version: %w", err)
+ }
+ createdImage, err := createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
+ if err != nil {
+ return "", fmt.Errorf("waiting for image version to be created: %w", err)
+ }
+ if createdImage.ID == nil {
+ return "", errors.New("created image has no id")
+ }
+ return *createdImage.ID, nil
+}
+
+func (u *Uploader) ensureImageVersionDeleted(ctx context.Context, sigName, definitionName, versionName string) error {
+ _, err := u.imageVersions.Get(ctx, u.resourceGroup, sigName, definitionName, versionName, &armcomputev4.GalleryImageVersionsClientGetOptions{})
+ if err != nil {
+ u.log.Debugf("Image version %s in %s/%s/%s doesn't exist. Nothing to clean up.", versionName, u.resourceGroup, sigName, definitionName)
+ return nil
+ }
+ u.log.Debugf("Deleting image version %s in %s/%s/%s", versionName, u.resourceGroup, sigName, definitionName)
+ deletePoller, err := u.imageVersions.BeginDelete(ctx, u.resourceGroup, sigName, definitionName, versionName, &armcomputev4.GalleryImageVersionsClientBeginDeleteOptions{})
+ if err != nil {
+ return fmt.Errorf("deleting image version: %w", err)
+ }
+ if _, err = deletePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
+ return fmt.Errorf("waiting for image version to be deleted: %w", err)
+ }
+ return nil
+}
+
+// getImageReference returns the image reference to use for the image version.
+// If the shared image gallery is a community gallery, the community identifier is returned.
+// Otherwise, the unshared identifier is returned.
+func (u *Uploader) getImageReference(ctx context.Context, sigName, definitionName, versionName, unsharedID string) (string, error) {
+ galleryResp, err := u.galleries.Get(ctx, u.resourceGroup, sigName, &armcomputev4.GalleriesClientGetOptions{})
+ if err != nil {
+ return "", fmt.Errorf("getting image gallery %s: %w", sigName, err)
+ }
+ if galleryResp.Properties == nil ||
+ galleryResp.Properties.SharingProfile == nil ||
+ galleryResp.Properties.SharingProfile.CommunityGalleryInfo == nil ||
+ galleryResp.Properties.SharingProfile.CommunityGalleryInfo.CommunityGalleryEnabled == nil ||
+ !*galleryResp.Properties.SharingProfile.CommunityGalleryInfo.CommunityGalleryEnabled {
+ u.log.Warnf("Image gallery %s in %s is not shared. Using private identifier", sigName, u.resourceGroup)
+ return unsharedID, nil
+ }
+ if galleryResp.Properties == nil ||
+ galleryResp.Properties.SharingProfile == nil ||
+ galleryResp.Properties.SharingProfile.CommunityGalleryInfo == nil ||
+ galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames == nil ||
+ len(galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames) < 1 ||
+ galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames[0] == nil {
+ return "", fmt.Errorf("image gallery %s in %s is a community gallery but has no public names", sigName, u.resourceGroup)
+ }
+ communityGalleryName := *galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames[0]
+ u.log.Debugf("Image gallery %s in %s is shared. Using community identifier in %s", sigName, u.resourceGroup, communityGalleryName)
+ communityVersionResp, err := u.communityVersions.Get(ctx, u.location, communityGalleryName,
+ definitionName, versionName,
+ &armcomputev4.CommunityGalleryImageVersionsClientGetOptions{},
+ )
+ if err != nil {
+ return "", fmt.Errorf("getting community image version %s/%s/%s: %w", communityGalleryName, definitionName, versionName, err)
+ }
+ if communityVersionResp.Identifier == nil || communityVersionResp.Identifier.UniqueID == nil {
+ return "", fmt.Errorf("community image version %s/%s/%s has no id", communityGalleryName, definitionName, versionName)
+ }
+ return *communityVersionResp.Identifier.UniqueID, nil
+}
+
+func uploadBlob(ctx context.Context, sasURL string, disk io.ReadSeeker, size int64, uploader sasBlobUploader) error {
+ uploadClient, err := uploader(sasURL)
+ if err != nil {
+ return fmt.Errorf("uploading blob: %w", err)
+ }
+ var offset int64
+ var chunksize int
+ chunk := make([]byte, pageSizeMax)
+ var readErr error
+ for offset < size {
+ chunksize, readErr = io.ReadAtLeast(disk, chunk, 1)
+ if readErr != nil {
+ return fmt.Errorf("reading from disk: %w", err)
+ }
+ if err := uploadChunk(ctx, uploadClient, bytes.NewReader(chunk[:chunksize]), offset, int64(chunksize)); err != nil {
+ return fmt.Errorf("uploading chunk: %w", err)
+ }
+ offset += int64(chunksize)
+ }
+ return nil
+}
+
+func uploadChunk(ctx context.Context, uploader azurePageblobAPI, chunk io.ReadSeeker, offset, chunksize int64) error {
+ _, err := uploader.UploadPages(ctx, &readSeekNopCloser{chunk}, blob.HTTPRange{
+ Offset: offset,
+ Count: chunksize,
+ }, nil)
+ return err
+}
+
+func imageOffer(version versionsapi.Version) string {
+ switch {
+ case version.Stream == "stable":
+ return "constellation"
+ case version.Stream == "debug" && version.Ref == "-":
+ return version.Version
+ }
+ return version.Ref + "-" + version.Stream
+}
+
+// imageVersion determines the semantic version string used inside a sig image.
+// For releases, the actual semantic version of the image (without leading v) is used (major.minor.patch).
+// Otherwise, the version is derived from the commit timestamp.
+func imageVersion(version versionsapi.Version, timestamp time.Time) (string, error) {
+ switch {
+ case version.Stream == "stable":
+ fallthrough
+ case version.Stream == "debug" && version.Ref == "-":
+ return strings.TrimLeft(version.Version, "v"), nil
+ }
+
+ formattedTime := timestamp.Format(timestampFormat)
+ if len(formattedTime) != len(timestampFormat) {
+ return "", errors.New("invalid timestamp")
+ }
+ // ..