constellation/internal/config/config.go
Daniel Weiße b966f57a2f
AB#2554 GCP CSI driver deployment (#532)
* Allow enabling/disabling of CSI driver through config

* Fix inconsistent namespace parsing

* Deploy GCP CSI driver on init

* Update invalid pod tolerations

* Add generate script for CSI charts

* Update generateCilium script

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
2022-11-18 10:05:02 +01:00

588 lines
24 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// This binary can be build from siderolabs/talos projects. Located at:
// https://github.com/siderolabs/talos/tree/master/hack/docgen
//
//go:generate docgen ./config.go ./config_doc.go Configuration
package config
import (
"errors"
"fmt"
"io/fs"
"os"
"regexp"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
"go.uber.org/multierr"
)
// Measurements is a required alias since docgen is not able to work with
// types in other packages.
type Measurements = measurements.M
const (
// Version1 is the first version number for Constellation config file.
Version1 = "v1"
)
var (
azureReleaseImageRegex = regexp.MustCompile(`^(?i)\/CommunityGalleries\/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df\/Images\/constellation\/Versions\/[\d]+.[\d]+.[\d]+$`)
gcpReleaseImageRegex = regexp.MustCompile(`^projects\/constellation-images\/global\/images\/constellation-v[\d]+-[\d]+-[\d]+$`)
)
// Config defines configuration used by CLI.
type Config struct {
// description: |
// Schema version of this configuration file.
Version string `yaml:"version" validate:"eq=v1"`
// description: |
// Size (in GB) of a node's disk to store the non-volatile state.
StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"`
// description: |
// Kubernetes version to be installed in the cluster.
KubernetesVersion string `yaml:"kubernetesVersion" validate:"supported_k8s_version"`
// description: |
// DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md
DebugCluster *bool `yaml:"debugCluster" validate:"required"`
// description: |
// Supported cloud providers and their specific configurations.
Provider ProviderConfig `yaml:"provider" validate:"dive"`
// description: |
// Deprecated: Does nothing! To get node SSH access, see: https://constellation-docs.edgeless.systems/constellation/workflows/troubleshooting#connect-to-nodes-via-ssh
// examples:
// - value: '[]UserKey{ { Username: "Alice", PublicKey: "ssh-rsa AAAAB3NzaC...5QXHKW1rufgtJeSeJ8= alice@domain.com" } }'
SSHUsers []UserKey `yaml:"sshUsers,omitempty" validate:"dive"`
// description: |
// Configuration to apply during constellation upgrade.
// examples:
// - value: 'UpgradeConfig{ Image: "", Measurements: Measurements{} }'
Upgrade UpgradeConfig `yaml:"upgrade,omitempty"`
}
// UpgradeConfig defines configuration used during constellation upgrade.
type UpgradeConfig struct {
// description: |
// Updated machine image to install on all nodes.
Image string `yaml:"image"`
// description: |
// Measurements of the updated image.
Measurements Measurements `yaml:"measurements"`
}
// UserKey describes a user that should be created with corresponding public SSH key.
//
// Deprecated: UserKey was used as configuration for access-manager, which was removed
// in v2.2, but config needs to retain these values for backwards compatibility and
// config validation.
type UserKey struct {
// description: |
// Username of new SSH user.
//
// Deprecated: See UserKey.
Username string `yaml:"username" validate:"required"`
// description: |
// Public key of new SSH user.
//
// Deprecated: See UserKey.
PublicKey string `yaml:"publicKey" validate:"required"`
}
// ProviderConfig are cloud-provider specific configuration values used by the CLI.
// Fields should remain pointer-types so custom specific configs can nil them
// if not required.
type ProviderConfig struct {
// description: |
// Configuration for AWS as provider.
AWS *AWSConfig `yaml:"aws,omitempty" validate:"omitempty,dive"`
// description: |
// Configuration for Azure as provider.
Azure *AzureConfig `yaml:"azure,omitempty" validate:"omitempty,dive"`
// description: |
// Configuration for Google Cloud as provider.
GCP *GCPConfig `yaml:"gcp,omitempty" validate:"omitempty,dive"`
// description: |
// Configuration for QEMU as provider.
QEMU *QEMUConfig `yaml:"qemu,omitempty" validate:"omitempty,dive"`
}
// AWSConfig are AWS specific configuration values used by the CLI.
type AWSConfig struct {
// description: |
// AWS data center region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions
Region string `yaml:"region" validate:"required"`
// description: |
// AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones
Zone string `yaml:"zone" validate:"required"`
// description: |
// AMI ID of the machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html
InstanceType string `yaml:"instanceType" validate:"lowercase,aws_instance_type"`
// description: |
// Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html
StateDiskType string `yaml:"stateDiskType" validate:"oneof=standard gp2 gp3 st1 sc1 io1"`
// description: |
// Name of the IAM profile to use for the control plane nodes.
IAMProfileControlPlane string `yaml:"iamProfileControlPlane" validate:"required"`
// description: |
// Name of the IAM profile to use for the worker nodes.
IAMProfileWorkerNodes string `yaml:"iamProfileWorkerNodes" validate:"required"`
// description: |
// Expected VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
}
// AzureConfig are Azure specific configuration values used by the CLI.
type AzureConfig struct {
// description: |
// Subscription ID of the used Azure account. See: https://docs.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id#find-your-azure-subscription
SubscriptionID string `yaml:"subscription" validate:"uuid"`
// description: |
// Tenant ID of the used Azure account. See: https://docs.microsoft.com/en-us/azure/azure-portal/get-subscription-tenant-id#find-your-azure-ad-tenant
TenantID string `yaml:"tenant" validate:"uuid"`
// description: |
// Azure datacenter region to be used. See: https://docs.microsoft.com/en-us/azure/availability-zones/az-overview#azure-regions-with-availability-zones
Location string `yaml:"location" validate:"required"`
// description: |
// Resource group for the cluster's resources. Must already exist.
ResourceGroup string `yaml:"resourceGroup" validate:"required"`
// description: |
// Authorize spawned VMs to access Azure API.
UserAssignedIdentity string `yaml:"userAssignedIdentity" validate:"required"`
// description: |
// Application client ID of the Active Directory app registration.
AppClientID string `yaml:"appClientID" validate:"uuid"`
// description: |
// Client secret value of the Active Directory app registration credentials. Alternatively leave empty and pass value via CONSTELL_AZURE_CLIENT_SECRET_VALUE environment variable.
ClientSecretValue string `yaml:"clientSecretValue" validate:"required"`
// description: |
// Machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes.
InstanceType string `yaml:"instanceType" validate:"azure_instance_type"`
// description: |
// Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#disk-type-comparison
StateDiskType string `yaml:"stateDiskType" validate:"oneof=Premium_LRS Premium_ZRS Standard_LRS StandardSSD_LRS StandardSSD_ZRS"`
// description: |
// Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
// description: |
// Use Confidential VMs. If set to false, Trusted Launch VMs are used instead. See: https://docs.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview
ConfidentialVM *bool `yaml:"confidentialVM" validate:"required"`
// description: |
// Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob.
SecureBoot *bool `yaml:"secureBoot" validate:"required"`
// description: |
// Expected value for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf
IDKeyDigest string `yaml:"idKeyDigest" validate:"required_if=EnforceIdKeyDigest true,omitempty,hexadecimal,len=96"`
// description: |
// Enforce the specified idKeyDigest value during remote attestation.
EnforceIDKeyDigest *bool `yaml:"enforceIdKeyDigest" validate:"required"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
}
// GCPConfig are GCP specific configuration values used by the CLI.
type GCPConfig struct {
// description: |
// GCP project. See: https://support.google.com/googleapi/answer/7014113?hl=en
Project string `yaml:"project" validate:"required"`
// description: |
// GCP datacenter region. See: https://cloud.google.com/compute/docs/regions-zones#available
Region string `yaml:"region" validate:"required"`
// description: |
// GCP datacenter zone. See: https://cloud.google.com/compute/docs/regions-zones#available
Zone string `yaml:"zone" validate:"required"`
// description: |
// Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization
ServiceAccountKeyPath string `yaml:"serviceAccountKeyPath" validate:"required"`
// description: |
// Machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes.
InstanceType string `yaml:"instanceType" validate:"gcp_instance_type"`
// description: |
// Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types
StateDiskType string `yaml:"stateDiskType" validate:"oneof=pd-standard pd-balanced pd-ssd"`
// description: |
// Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage
DeployCSIDriver *bool `yaml:"deployCSIDriver" validate:"required"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
}
// QEMUConfig holds config information for QEMU based Constellation deployments.
type QEMUConfig struct {
// description: |
// Path to the image to use for the VMs.
Image string `yaml:"image" validate:"required"`
// description: |
// Format of the image to use for the VMs. Should be either qcow2 or raw.
ImageFormat string `yaml:"imageFormat" validate:"oneof=qcow2 raw"`
// description: |
// vCPU count for the VMs.
VCPUs int `yaml:"vcpus" validate:"required"`
// description: |
// Amount of memory per instance (MiB).
Memory int `yaml:"memory" validate:"required"`
// description: |
// Container image to use for the QEMU metadata server.
MetadataAPIImage string `yaml:"metadataAPIServer" validate:"required"`
// description: |
// Libvirt connection URI. Leave empty to start a libvirt instance in Docker.
LibvirtURI string `yaml:"libvirtSocket"`
// description: |
// Container image to use for launching a containerized libvirt daemon. Only relevant if `libvirtSocket = ""`.
LibvirtContainerImage string `yaml:"libvirtContainerImage"`
// description: |
// NVRAM template to be used for secure boot. Can be sentinel value "production", "testing" or a path to a custom NVRAM template
NVRAM string `yaml:"nvram" validate:"required"`
// description: |
// Path to the OVMF firmware. Leave empty for auto selection.
Firmware string `yaml:"firmware"`
// description: |
// Measurement used to enable measured boot.
Measurements Measurements `yaml:"measurements"`
// description: |
// List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning.
EnforcedMeasurements []uint32 `yaml:"enforcedMeasurements"`
}
// Default returns a struct with the default config.
func Default() *Config {
return &Config{
Version: Version1,
StateDiskSizeGB: 30,
DebugCluster: func() *bool { b := false; return &b }(),
Provider: ProviderConfig{
AWS: &AWSConfig{
Region: "",
Image: "",
InstanceType: "m6a.xlarge",
StateDiskType: "gp3",
IAMProfileControlPlane: "",
IAMProfileWorkerNodes: "",
Measurements: measurements.DefaultsFor(cloudprovider.AWS),
EnforcedMeasurements: []uint32{4, 8, 9, 11, 12, 13, 15},
},
Azure: &AzureConfig{
SubscriptionID: "",
TenantID: "",
Location: "",
UserAssignedIdentity: "",
ResourceGroup: "",
Image: DefaultImageAzure,
InstanceType: "Standard_DC4as_v5",
StateDiskType: "Premium_LRS",
DeployCSIDriver: func() *bool { b := true; return &b }(),
IDKeyDigest: "57486a447ec0f1958002a22a06b7673b9fd27d11e1c6527498056054c5fa92d23c50f9de44072760fe2b6fb89740b696",
EnforceIDKeyDigest: func() *bool { b := true; return &b }(),
ConfidentialVM: func() *bool { b := true; return &b }(),
SecureBoot: func() *bool { b := false; return &b }(),
Measurements: measurements.DefaultsFor(cloudprovider.Azure),
EnforcedMeasurements: []uint32{4, 8, 9, 11, 12, 13, 15},
},
GCP: &GCPConfig{
Project: "",
Region: "",
Zone: "",
ServiceAccountKeyPath: "",
Image: DefaultImageGCP,
InstanceType: "n2d-standard-4",
StateDiskType: "pd-ssd",
DeployCSIDriver: func() *bool { b := true; return &b }(),
Measurements: measurements.DefaultsFor(cloudprovider.GCP),
EnforcedMeasurements: []uint32{0, 4, 8, 9, 11, 12, 13, 15},
},
QEMU: &QEMUConfig{
ImageFormat: "raw",
VCPUs: 2,
Memory: 2048,
MetadataAPIImage: versions.QEMUMetadataImage,
LibvirtURI: "",
LibvirtContainerImage: versions.LibvirtImage,
NVRAM: "production",
Measurements: measurements.DefaultsFor(cloudprovider.QEMU),
EnforcedMeasurements: []uint32{4, 8, 9, 11, 12, 13, 15},
},
},
KubernetesVersion: string(versions.Default),
}
}
// FromFile returns config file with `name` read from `fileHandler` by parsing
// it as YAML. You should prefer config.New to read env vars and validate
// config in a consistent manner.
func FromFile(fileHandler file.Handler, name string) (*Config, error) {
var conf Config
if err := fileHandler.ReadYAMLStrict(name, &conf); err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("unable to find %s - use `constellation config generate` to generate it first", name)
}
return nil, fmt.Errorf("could not load config from file %s: %w", name, err)
}
return &conf, nil
}
// New creates a new config by:
// 1. Reading config file via provided fileHandler from file with name.
// 2. Read secrets from environment variables.
// 3. Validate config.
func New(fileHandler file.Handler, name string) (*Config, error) {
// Read config file
c, err := FromFile(fileHandler, name)
if err != nil {
return nil, err
}
// Read secrets from env-vars.
clientSecretValue := os.Getenv(constants.EnvVarAzureClientSecretValue)
if clientSecretValue != "" && c.Provider.Azure != nil {
c.Provider.Azure.ClientSecretValue = clientSecretValue
}
return c, c.Validate()
}
// HasProvider checks whether the config contains the provider.
func (c *Config) HasProvider(provider cloudprovider.Provider) bool {
switch provider {
case cloudprovider.AWS:
return c.Provider.AWS != nil
case cloudprovider.Azure:
return c.Provider.Azure != nil
case cloudprovider.GCP:
return c.Provider.GCP != nil
case cloudprovider.QEMU:
return c.Provider.QEMU != nil
}
return false
}
// Image returns OS image for the configured cloud provider.
// If multiple cloud providers are configured (which is not supported)
// only a single image is returned.
func (c *Config) Image() string {
if c.HasProvider(cloudprovider.AWS) {
return c.Provider.AWS.Image
}
if c.HasProvider(cloudprovider.Azure) {
return c.Provider.Azure.Image
}
if c.HasProvider(cloudprovider.GCP) {
return c.Provider.GCP.Image
}
return ""
}
// UpdateMeasurements overwrites measurements in config with the provided ones.
func (c *Config) UpdateMeasurements(newMeasurements Measurements) {
if c.Provider.AWS != nil {
c.Provider.AWS.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.Azure != nil {
c.Provider.Azure.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.GCP != nil {
c.Provider.GCP.Measurements.CopyFrom(newMeasurements)
}
if c.Provider.QEMU != nil {
c.Provider.QEMU.Measurements.CopyFrom(newMeasurements)
}
}
// RemoveProviderExcept removes all provider specific configurations, i.e.,
// sets them to nil, except the one specified.
// If an unknown provider is passed, the same configuration is returned.
func (c *Config) RemoveProviderExcept(provider cloudprovider.Provider) {
currentProviderConfigs := c.Provider
c.Provider = ProviderConfig{}
switch provider {
case cloudprovider.AWS:
c.Provider.AWS = currentProviderConfigs.AWS
case cloudprovider.Azure:
c.Provider.Azure = currentProviderConfigs.Azure
case cloudprovider.GCP:
c.Provider.GCP = currentProviderConfigs.GCP
case cloudprovider.QEMU:
c.Provider.QEMU = currentProviderConfigs.QEMU
default:
c.Provider = currentProviderConfigs
}
}
// IsAzureNonCVM checks whether the chosen provider is azure and confidential VMs are disabled.
func (c *Config) IsAzureNonCVM() bool {
return c.Provider.Azure != nil && c.Provider.Azure.ConfidentialVM != nil && !*c.Provider.Azure.ConfidentialVM
}
// IsDebugCluster checks whether the cluster is configured as a debug cluster.
func (c *Config) IsDebugCluster() bool {
if c.DebugCluster != nil && *c.DebugCluster {
return true
}
return false
}
// IsDebugImage checks whether image name looks like a release image, if not it is
// probably a debug image. In the end we do not if bootstrapper or debugd
// was put inside an image just by looking at its name.
func (c *Config) IsDebugImage() bool {
switch {
case c.Provider.AWS != nil:
// TODO: Add proper image name validation for AWS as part of rfc/image-discoverability.md
return false
case c.Provider.Azure != nil:
return !azureReleaseImageRegex.MatchString(c.Provider.Azure.Image)
case c.Provider.GCP != nil:
return !gcpReleaseImageRegex.MatchString(c.Provider.GCP.Image)
default:
return false
}
}
// GetProvider returns the configured cloud provider.
func (c *Config) GetProvider() cloudprovider.Provider {
if c.Provider.AWS != nil {
return cloudprovider.AWS
}
if c.Provider.Azure != nil {
return cloudprovider.Azure
}
if c.Provider.GCP != nil {
return cloudprovider.GCP
}
if c.Provider.QEMU != nil {
return cloudprovider.QEMU
}
return cloudprovider.Unknown
}
// EnforcesIDKeyDigest checks whether ID Key Digest should be enforced for respective cloud provider.
func (c *Config) EnforcesIDKeyDigest() bool {
return c.Provider.Azure != nil && c.Provider.Azure.EnforceIDKeyDigest != nil && *c.Provider.Azure.EnforceIDKeyDigest
}
// GetEnforcedPCRs returns the list of enforced PCRs for the configured cloud provider.
func (c *Config) GetEnforcedPCRs() []uint32 {
provider := c.GetProvider()
switch provider {
case cloudprovider.AWS:
return c.Provider.AWS.EnforcedMeasurements
case cloudprovider.Azure:
return c.Provider.Azure.EnforcedMeasurements
case cloudprovider.GCP:
return c.Provider.GCP.EnforcedMeasurements
case cloudprovider.QEMU:
return c.Provider.QEMU.EnforcedMeasurements
default:
return nil
}
}
// DeployCSIDriver returns whether the CSI driver should be deployed for a given cloud provider.
func (c *Config) DeployCSIDriver() bool {
return c.Provider.Azure != nil && c.Provider.Azure.DeployCSIDriver != nil && *c.Provider.Azure.DeployCSIDriver ||
c.Provider.GCP != nil && c.Provider.GCP.DeployCSIDriver != nil && *c.Provider.GCP.DeployCSIDriver
}
// Validate checks the config values and returns validation errors.
func (c *Config) Validate() error {
trans := ut.New(en.New()).GetFallback()
validate := validator.New()
if err := en_translations.RegisterDefaultTranslations(validate, trans); err != nil {
return err
}
// Register AWS, Azure & GCP InstanceType validation error types
if err := validate.RegisterTranslation("aws_instance_type", trans, registerTranslateAWSInstanceTypeError, translateAWSInstanceTypeError); err != nil {
return err
}
if err := validate.RegisterTranslation("azure_instance_type", trans, registerTranslateAzureInstanceTypeError, c.translateAzureInstanceTypeError); err != nil {
return err
}
if err := validate.RegisterTranslation("gcp_instance_type", trans, registerTranslateGCPInstanceTypeError, translateGCPInstanceTypeError); err != nil {
return err
}
// Register Provider validation error types
if err := validate.RegisterTranslation("no_provider", trans, registerNoProviderError, translateNoProviderError); err != nil {
return err
}
if err := validate.RegisterTranslation("more_than_one_provider", trans, registerMoreThanOneProviderError, c.translateMoreThanOneProviderError); err != nil {
return err
}
// register custom validator with label supported_k8s_version to validate version based on available versionConfigs.
if err := validate.RegisterValidation("supported_k8s_version", validateK8sVersion); err != nil {
return err
}
// register custom validator with label aws_instance_type to validate the AWS instance type from config input.
if err := validate.RegisterValidation("aws_instance_type", validateAWSInstanceType); err != nil {
return err
}
// register custom validator with label azure_instance_type to validate the Azure instance type from config input.
if err := validate.RegisterValidation("azure_instance_type", validateAzureInstanceType); err != nil {
return err
}
// register custom validator with label gcp_instance_type to validate the GCP instance type from config input.
if err := validate.RegisterValidation("gcp_instance_type", validateGCPInstanceType); err != nil {
return err
}
// Register provider validation
validate.RegisterStructValidation(validateProvider, ProviderConfig{})
err := validate.Struct(c)
if err == nil {
return nil
}
var errs validator.ValidationErrors
if !errors.As(err, &errs) {
return err
}
var validationErrors error
for _, e := range errs {
validationErrors = multierr.Append(
validationErrors,
errors.New(e.Translate(trans)),
)
}
return validationErrors
}