mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-15 10:54:29 -05:00
913b09aeb8
* terraform: enable creation of SEV-SNP VMs on GCP * variant: add SEV-SNP attestation variant * config: add SEV-SNP config options for GCP * measurements: add GCP SEV-SNP measurements * gcp: separate package for SEV-ES * attestation: add GCP SEV-SNP attestation logic * gcp: factor out common logic * choose: add GCP SEV-SNP * cli: add TF variable passthrough for GCP SEV-SNP variables * cli: support GCP SEV-SNP for `constellation verify` * Adjust usage of GCP SEV-SNP throughout codebase * ci: add GCP SEV-SNP * terraform-provider: support GCP SEV-SNP * docs: add GCP SEV-SNP reference * linter fixes * gcp: only run test with TPM simulator * gcp: remove nonsense test * Update cli/internal/cmd/verify.go Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update docs/docs/overview/clouds.md Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * Update terraform-provider-constellation/internal/provider/attestation_data_source_test.go Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com> * linter fixes * terraform_provider: correctly pass down CC technology * config: mark attestationconfigapi as unimplemented * gcp: fix comments and typos * snp: use nonce and PK hash in SNP report * snp: ensure we never use ARK supplied by Issuer (#3025) * Make sure SNP ARK is always loaded from config, or fetched from AMD KDS * GCP: Set validator `reportData` correctly --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems> Co-authored-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * attestationconfigapi: add GCP to uploading * snp: use correct cert Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * terraform-provider: enable fetching of attestation config values for GCP SEV-SNP * linter fixes --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems> Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
852 lines
28 KiB
Go
852 lines
28 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
ut "github.com/go-playground/universal-translator"
|
|
"github.com/go-playground/validator/v10"
|
|
"golang.org/x/mod/semver"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
|
"github.com/edgelesssys/constellation/v2/internal/compatibility"
|
|
"github.com/edgelesssys/constellation/v2/internal/config/disktypes"
|
|
"github.com/edgelesssys/constellation/v2/internal/config/instancetypes"
|
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
|
consemver "github.com/edgelesssys/constellation/v2/internal/semver"
|
|
"github.com/edgelesssys/constellation/v2/internal/versions"
|
|
)
|
|
|
|
// ValidationError occurs when the validation of a config fails.
|
|
// It contains a list of errors that occurred during validation.
|
|
type ValidationError struct {
|
|
validationErrMsgs []string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return "invalid configuration"
|
|
}
|
|
|
|
// LongMessage prints the errors that occurred during validation in a verbose and user friendly way.
|
|
func (e *ValidationError) LongMessage() string {
|
|
msg := "Problems validating config file:\n"
|
|
for _, ve := range e.validationErrMsgs {
|
|
msg += fmt.Sprintf("\t%s\n", ve)
|
|
}
|
|
msg += "Fix the invalid entries or generate a new configuration using `constellation config generate`"
|
|
return msg
|
|
}
|
|
|
|
func (e *ValidationError) messagesCount() int {
|
|
return len(e.validationErrMsgs)
|
|
}
|
|
|
|
func registerInvalidK8sVersionError(ut ut.Translator) error {
|
|
return ut.Add("supported_k8s_version", "{0} specifies an unsupported Kubernetes version. {1}", true)
|
|
}
|
|
|
|
func translateInvalidK8sVersionError(ut ut.Translator, fe validator.FieldError) string {
|
|
validVersions := make([]string, len(versions.VersionConfigs))
|
|
i := 0
|
|
for k := range versions.VersionConfigs {
|
|
validVersions[i] = string(k)
|
|
i++
|
|
}
|
|
validVersionsSorted := semver.ByVersion(validVersions)
|
|
sort.Sort(validVersionsSorted)
|
|
|
|
if len(validVersionsSorted) == 0 {
|
|
t, _ := ut.T("supported_k8s_version", fe.Field(), "No valid versions available. This should never happen")
|
|
return t
|
|
}
|
|
maxVersion := validVersionsSorted[len(validVersionsSorted)-1]
|
|
minVersion := validVersionsSorted[0]
|
|
|
|
var errorMsg string
|
|
configured, ok := fe.Value().(string)
|
|
if !ok {
|
|
errorMsg = fmt.Sprintf("The configured version is not a valid string. Supported versions: %s", strings.Join(validVersionsSorted, " "))
|
|
t, _ := ut.T("supported_k8s_version", fe.Field(), errorMsg)
|
|
return t
|
|
}
|
|
|
|
configured = compatibility.EnsurePrefixV(configured)
|
|
switch {
|
|
case !semver.IsValid(configured):
|
|
errorMsg = "The configured version is not a valid semantic version\n"
|
|
case semver.Compare(configured, minVersion) == -1:
|
|
errorMsg = fmt.Sprintf("The configured version %s is older than the oldest version supported by this CLI: %s\n", configured, minVersion)
|
|
case semver.Compare(configured, maxVersion) == 1:
|
|
errorMsg = fmt.Sprintf("The configured version %s is newer than the newest version supported by this CLI: %s\n", configured, maxVersion)
|
|
}
|
|
|
|
errorMsg = errorMsg + fmt.Sprintf("Supported versions: %s", strings.Join(validVersionsSorted, " "))
|
|
|
|
t, _ := ut.T("supported_k8s_version", fe.Field(), errorMsg)
|
|
|
|
return t
|
|
}
|
|
|
|
func validateAWSRegionField(fl validator.FieldLevel) bool {
|
|
return ValidateAWSRegion(fl.Field().String())
|
|
}
|
|
|
|
func validateAWSZoneField(fl validator.FieldLevel) bool {
|
|
return ValidateAWSZone(fl.Field().String())
|
|
}
|
|
|
|
func validateAzureZoneField(fl validator.FieldLevel) bool {
|
|
return ValidateAzureZone(fl.Field().String())
|
|
}
|
|
|
|
func validateGCPZoneField(fl validator.FieldLevel) bool {
|
|
return ValidateGCPZone(fl.Field().String())
|
|
}
|
|
|
|
func validateOpenStackRegionField(fl validator.FieldLevel) bool {
|
|
return ValidateOpenStackRegion(fl.Field().String())
|
|
}
|
|
|
|
// ValidateAWSZone validates that the zone is in the correct format.
|
|
func ValidateAWSZone(zone string) bool {
|
|
awsZoneRegex := regexp.MustCompile(`^\w+-\w+-[1-9][abc]$`)
|
|
return awsZoneRegex.MatchString(zone)
|
|
}
|
|
|
|
// ValidateAzureZone validates that the zone is in the correct format.
|
|
func ValidateAzureZone(zone string) bool {
|
|
azureZoneRegex := regexp.MustCompile(`^$|^\d+(?:,\d+)*$`)
|
|
return azureZoneRegex.MatchString(zone)
|
|
}
|
|
|
|
// ValidateGCPZone validates that the zone is in the correct format.
|
|
func ValidateGCPZone(zone string) bool {
|
|
gcpZoneRegex := regexp.MustCompile(`^[\w-]+-[a-z]$`)
|
|
return gcpZoneRegex.MatchString(zone)
|
|
}
|
|
|
|
// ValidateOpenStackRegion validates that the region is in the correct format.
|
|
func ValidateOpenStackRegion(region string) bool {
|
|
return len(region) > 0
|
|
}
|
|
|
|
// ValidateAWSRegion validates that the region is in the correct format.
|
|
func ValidateAWSRegion(region string) bool {
|
|
awsRegionRegex := regexp.MustCompile(`^\w+-\w+-[1-9]$`)
|
|
return awsRegionRegex.MatchString(region)
|
|
}
|
|
|
|
// validateProvider checks if zero or more than one providers are defined in the config.
|
|
func validateProvider(sl validator.StructLevel) {
|
|
provider := sl.Current().Interface().(ProviderConfig)
|
|
providerCount := 0
|
|
|
|
if provider.AWS != nil {
|
|
providerCount++
|
|
}
|
|
if provider.Azure != nil {
|
|
providerCount++
|
|
}
|
|
if provider.GCP != nil {
|
|
providerCount++
|
|
}
|
|
if provider.OpenStack != nil {
|
|
providerCount++
|
|
}
|
|
if provider.QEMU != nil {
|
|
providerCount++
|
|
}
|
|
|
|
if providerCount < 1 {
|
|
sl.ReportError(provider, "Provider", "Provider", "no_provider", "")
|
|
} else if providerCount > 1 {
|
|
sl.ReportError(provider, "Provider", "Provider", "more_than_one_provider", "")
|
|
}
|
|
}
|
|
|
|
func validateAttestation(sl validator.StructLevel) {
|
|
attestation := sl.Current().Interface().(AttestationConfig)
|
|
attestationCount := 0
|
|
|
|
if attestation.AWSSEVSNP != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.AWSNitroTPM != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.AzureSEVSNP != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.AzureTDX != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.AzureTrustedLaunch != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.GCPSEVES != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.GCPSEVSNP != nil {
|
|
attestationCount++
|
|
}
|
|
if attestation.QEMUVTPM != nil {
|
|
attestationCount++
|
|
}
|
|
|
|
if attestationCount < 1 {
|
|
sl.ReportError(attestation, "Attestation", "Attestation", "no_attestation", "")
|
|
} else if attestationCount > 1 {
|
|
sl.ReportError(attestation, "Attestation", "Attestation", "more_than_one_attestation", "")
|
|
}
|
|
}
|
|
|
|
func validateNodeGroups(sl validator.StructLevel) {
|
|
nodeGroups := sl.Current().Interface().(Config).NodeGroups
|
|
defaultControlPlaneGroup, hasDefaultControlPlaneGroup := nodeGroups[constants.DefaultControlPlaneGroupName]
|
|
defaultWorkerGroup, hasDefaultWorkerGroup := nodeGroups[constants.DefaultWorkerGroupName]
|
|
|
|
if !hasDefaultControlPlaneGroup {
|
|
sl.ReportError(nodeGroups, "NodeGroups", "NodeGroups", "no_default_control_plane_group", "")
|
|
}
|
|
if !hasDefaultWorkerGroup {
|
|
sl.ReportError(nodeGroups, "NodeGroups", "NodeGroups", "no_default_worker_group", "")
|
|
}
|
|
|
|
if hasDefaultControlPlaneGroup {
|
|
if defaultControlPlaneGroup.Role != role.ControlPlane.TFString() {
|
|
sl.ReportError(nodeGroups, "NodeGroups", "NodeGroups", "control_plane_group_role_mismatch", "")
|
|
}
|
|
if defaultControlPlaneGroup.InitialCount < 1 {
|
|
sl.ReportError(nodeGroups, "NodeGroups", "NodeGroups", "control_plane_group_initial_count", "")
|
|
}
|
|
}
|
|
if hasDefaultWorkerGroup {
|
|
if defaultWorkerGroup.Role != role.Worker.TFString() {
|
|
sl.ReportError(nodeGroups, "NodeGroups", "NodeGroups", "worker_group_role_mismatch", "")
|
|
}
|
|
}
|
|
}
|
|
|
|
func translateNoAttestationError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("no_attestation", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerNoAttestationError(ut ut.Translator) error {
|
|
return ut.Add("no_attestation", "{0}: No attestation has been defined (requires either awsSEVSNP, awsNitroTPM, azureSEVSNP, azureTDX, azureTrustedLaunch, gcpSEVES, or qemuVTPM)", true)
|
|
}
|
|
|
|
func translateNoDefaultControlPlaneGroupError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("no_default_control_plane_group", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerNoDefaultControlPlaneGroupError(ut ut.Translator) error {
|
|
return ut.Add("no_default_control_plane_group", "{0}: No default control plane group (control_plane_default) has been defined", true)
|
|
}
|
|
|
|
func translateNoDefaultWorkerGroupError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("no_default_worker_group", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerNoDefaultWorkerGroupError(ut ut.Translator) error {
|
|
return ut.Add("no_default_worker_group", "{0}: No default worker group (worker_default) has been defined", true)
|
|
}
|
|
|
|
func translateControlPlaneGroupInitialCountError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("control_plane_group_initial_count", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerControlPlaneGroupInitialCountError(ut ut.Translator) error {
|
|
return ut.Add("control_plane_group_initial_count", "{0}: The default control plane group (control_plane_default) must have at least one node", true)
|
|
}
|
|
|
|
func translateControlPlaneGroupRoleMismatchError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("control_plane_group_role_mismatch", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerControlPlaneGroupRoleMismatchError(ut ut.Translator) error {
|
|
return ut.Add("control_plane_group_role_mismatch", "{0}: The default control plane group (control_plane_default) must have the \"control-plane\" role", true)
|
|
}
|
|
|
|
func translateWorkerGroupRoleMismatchError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("worker_group_role_mismatch", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerWorkerGroupRoleMismatchError(ut ut.Translator) error {
|
|
return ut.Add("worker_group_role_mismatch", "{0}: The default worker group (worker_default) must have the \"worker\" role", true)
|
|
}
|
|
|
|
func registerValidZoneError(ut ut.Translator) error {
|
|
return ut.Add("valid_zone", "{0}: has invalid format: {1}", true)
|
|
}
|
|
|
|
func (c *Config) translateValidZoneError(ut ut.Translator, fe validator.FieldError) string {
|
|
var t string
|
|
switch c.GetProvider() {
|
|
case cloudprovider.AWS:
|
|
t, _ = ut.T("valid_zone", fe.Field(), "field must be of format eu-central-1a")
|
|
case cloudprovider.Azure:
|
|
t, _ = ut.T("valid_zone", fe.Field(), "field must be a comma separated list of zones: 1,2,3 (or empty for all zones)")
|
|
case cloudprovider.GCP:
|
|
t, _ = ut.T("valid_zone", fe.Field(), "field must be of format europe-west3-b")
|
|
default:
|
|
t, _ = ut.T("valid_zone", fe.Field(), "field must not be empty")
|
|
}
|
|
return t
|
|
}
|
|
|
|
func registerAWSRegionError(ut ut.Translator) error {
|
|
return ut.Add("aws_region", "{0}: has invalid format: {1}", true)
|
|
}
|
|
|
|
func translateAWSRegionError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("aws_region", fe.Field(), "field must be of format eu-central-1")
|
|
|
|
return t
|
|
}
|
|
|
|
func translateAWSZoneError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("aws_zone", fe.Field(), "field must be of format eu-central-1a")
|
|
|
|
return t
|
|
}
|
|
|
|
func registerAWSZoneError(ut ut.Translator) error {
|
|
return ut.Add("aws_zone", "{0}: has invalid format: {1}", true)
|
|
}
|
|
|
|
func registerMoreThanOneAttestationError(ut ut.Translator) error {
|
|
return ut.Add("more_than_one_attestation", "{0}: Only one attestation can be defined ({1} are defined)", true)
|
|
}
|
|
|
|
func (c *Config) translateMoreThanOneAttestationError(ut ut.Translator, fe validator.FieldError) string {
|
|
definedAttestations := make([]string, 0)
|
|
|
|
if c.Attestation.AWSNitroTPM != nil {
|
|
definedAttestations = append(definedAttestations, "AWSNitroTPM")
|
|
}
|
|
if c.Attestation.AWSSEVSNP != nil {
|
|
definedAttestations = append(definedAttestations, "AWSSEVSNP")
|
|
}
|
|
if c.Attestation.AzureSEVSNP != nil {
|
|
definedAttestations = append(definedAttestations, "AzureSEVSNP")
|
|
}
|
|
if c.Attestation.AzureTDX != nil {
|
|
definedAttestations = append(definedAttestations, "AzureTDX")
|
|
}
|
|
if c.Attestation.AzureTrustedLaunch != nil {
|
|
definedAttestations = append(definedAttestations, "AzureTrustedLaunch")
|
|
}
|
|
if c.Attestation.GCPSEVES != nil {
|
|
definedAttestations = append(definedAttestations, "GCPSEVES")
|
|
}
|
|
if c.Attestation.GCPSEVSNP != nil {
|
|
definedAttestations = append(definedAttestations, "GCPSEVSNP")
|
|
}
|
|
if c.Attestation.QEMUVTPM != nil {
|
|
definedAttestations = append(definedAttestations, "QEMUVTPM")
|
|
}
|
|
|
|
t, _ := ut.T("more_than_one_attestation", fe.Field(), strings.Join(definedAttestations, ", "))
|
|
|
|
return t
|
|
}
|
|
|
|
func registerTranslateDiskTypeError(ut ut.Translator) error {
|
|
return ut.Add("disk_type", "{0} must be one of {1}", true)
|
|
}
|
|
|
|
func (c *Config) translateDiskTypeError(ut ut.Translator, fe validator.FieldError) string {
|
|
var supported []string
|
|
switch c.GetProvider() {
|
|
case cloudprovider.AWS:
|
|
supported = disktypes.AWSDiskTypes
|
|
case cloudprovider.Azure:
|
|
supported = disktypes.AzureDiskTypes
|
|
case cloudprovider.GCP:
|
|
supported = disktypes.GCPDiskTypes
|
|
}
|
|
|
|
t, _ := ut.T("disk_type", fe.Field(), fmt.Sprintf("%v", supported))
|
|
return t
|
|
}
|
|
|
|
func (c *Config) registerTranslateInstanceTypeError(ut ut.Translator) error {
|
|
switch c.GetProvider() {
|
|
case cloudprovider.AWS:
|
|
return registerTranslateAWSInstanceTypeError(ut)
|
|
case cloudprovider.Azure:
|
|
return registerTranslateAzureInstanceTypeError(ut)
|
|
case cloudprovider.GCP:
|
|
return registerTranslateGCPInstanceTypeError(ut)
|
|
}
|
|
return ut.Add("instance_type", "{0} is an invalid instance type", true)
|
|
}
|
|
|
|
func (c *Config) translateInstanceTypeError(ut ut.Translator, fe validator.FieldError) string {
|
|
switch c.GetProvider() {
|
|
case cloudprovider.AWS:
|
|
return c.translateAWSInstanceTypeError(ut, fe)
|
|
case cloudprovider.Azure:
|
|
return c.translateAzureInstanceTypeError(ut, fe)
|
|
case cloudprovider.GCP:
|
|
return translateGCPInstanceTypeError(ut, fe)
|
|
}
|
|
t, _ := ut.T("instance_type", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerTranslateAWSInstanceTypeError(ut ut.Translator) error {
|
|
return ut.Add("instance_type", "{0} must be an instance from one of the following families types with size xlarge or higher: {1}", true)
|
|
}
|
|
|
|
func (c *Config) translateAWSInstanceTypeError(ut ut.Translator, fe validator.FieldError) string {
|
|
var t string
|
|
|
|
attestVariant := c.GetAttestationConfig().GetVariant()
|
|
|
|
instances := instancetypes.AWSSNPSupportedInstanceFamilies
|
|
if attestVariant.Equal(variant.AWSNitroTPM{}) {
|
|
instances = instancetypes.AWSSupportedInstanceFamilies
|
|
}
|
|
|
|
t, _ = ut.T("instance_type", fe.Field(), fmt.Sprintf("%v", instances))
|
|
|
|
return t
|
|
}
|
|
|
|
func registerTranslateGCPInstanceTypeError(ut ut.Translator) error {
|
|
return ut.Add("instance_type", fmt.Sprintf("{0} must be one of %v", instancetypes.GCPInstanceTypes), true)
|
|
}
|
|
|
|
func translateGCPInstanceTypeError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("instance_type", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
// Validation translation functions for Azure & GCP instance type errors.
|
|
func registerTranslateAzureInstanceTypeError(ut ut.Translator) error {
|
|
return ut.Add("instance_type", "{0} must be one of {1}", true)
|
|
}
|
|
|
|
func (c *Config) translateAzureInstanceTypeError(ut ut.Translator, fe validator.FieldError) string {
|
|
// Suggest trusted launch VMs if confidential VMs have been specifically disabled
|
|
var t string
|
|
|
|
attestVariant := c.GetAttestationConfig().GetVariant()
|
|
|
|
var instances []string
|
|
switch attestVariant.String() {
|
|
case variant.AzureTrustedLaunch{}.String():
|
|
instances = instancetypes.AzureTrustedLaunchInstanceTypes
|
|
case variant.AzureSEVSNP{}.String():
|
|
instances = instancetypes.AzureSNPInstanceTypes
|
|
case variant.AzureTDX{}.String():
|
|
instances = instancetypes.AzureTDXInstanceTypes
|
|
}
|
|
|
|
t, _ = ut.T("instance_type", fe.Field(), fmt.Sprintf("%v", instances))
|
|
|
|
return t
|
|
}
|
|
|
|
// Validation translation functions for Provider errors.
|
|
func registerNoProviderError(ut ut.Translator) error {
|
|
return ut.Add("no_provider", "{0}: No provider has been defined (requires either Azure, GCP, OpenStack or QEMU)", true)
|
|
}
|
|
|
|
func translateNoProviderError(ut ut.Translator, fe validator.FieldError) string {
|
|
t, _ := ut.T("no_provider", fe.Field())
|
|
|
|
return t
|
|
}
|
|
|
|
func registerMoreThanOneProviderError(ut ut.Translator) error {
|
|
return ut.Add("more_than_one_provider", "{0}: Only one provider can be defined ({1} are defined)", true)
|
|
}
|
|
|
|
func (c *Config) translateMoreThanOneProviderError(ut ut.Translator, fe validator.FieldError) string {
|
|
definedProviders := make([]string, 0)
|
|
|
|
// c.Provider should not be nil as Provider would need to be defined for the validation to fail in this place.
|
|
if c.Provider.AWS != nil {
|
|
definedProviders = append(definedProviders, "AWS")
|
|
}
|
|
if c.Provider.Azure != nil {
|
|
definedProviders = append(definedProviders, "Azure")
|
|
}
|
|
if c.Provider.GCP != nil {
|
|
definedProviders = append(definedProviders, "GCP")
|
|
}
|
|
if c.Provider.OpenStack != nil {
|
|
definedProviders = append(definedProviders, "OpenStack")
|
|
}
|
|
if c.Provider.QEMU != nil {
|
|
definedProviders = append(definedProviders, "QEMU")
|
|
}
|
|
|
|
// Show single string if only one other provider is defined, show list with brackets if multiple are defined.
|
|
t, _ := ut.T("more_than_one_provider", fe.Field(), strings.Join(definedProviders, ", "))
|
|
|
|
return t
|
|
}
|
|
|
|
func validInstanceTypeForProvider(insType string, attestation variant.Variant) bool {
|
|
switch attestation {
|
|
case variant.AWSSEVSNP{}, variant.AWSNitroTPM{}:
|
|
return isSupportedAWSInstanceType(insType, attestation.Equal(variant.AWSNitroTPM{}))
|
|
case variant.AzureSEVSNP{}:
|
|
for _, instanceType := range instancetypes.AzureSNPInstanceTypes {
|
|
if insType == instanceType {
|
|
return true
|
|
}
|
|
}
|
|
case variant.AzureTDX{}:
|
|
for _, instanceType := range instancetypes.AzureTDXInstanceTypes {
|
|
if insType == instanceType {
|
|
return true
|
|
}
|
|
}
|
|
case variant.AzureTrustedLaunch{}:
|
|
for _, instanceType := range instancetypes.AzureTrustedLaunchInstanceTypes {
|
|
if insType == instanceType {
|
|
return true
|
|
}
|
|
}
|
|
case variant.GCPSEVES{}, variant.GCPSEVSNP{}:
|
|
for _, instanceType := range instancetypes.GCPInstanceTypes {
|
|
if insType == instanceType {
|
|
return true
|
|
}
|
|
}
|
|
case variant.QEMUVTPM{}, variant.QEMUTDX{}:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isSupportedAWSInstanceType checks if an AWS instance type passed as user input is in one of the supported instance types.
|
|
func isSupportedAWSInstanceType(userInput string, acceptNonCVM bool) bool {
|
|
// Check if user or code does anything weird and tries to pass multiple strings as one
|
|
if strings.Contains(userInput, " ") {
|
|
return false
|
|
}
|
|
if strings.Contains(userInput, ",") {
|
|
return false
|
|
}
|
|
if strings.Contains(userInput, ";") {
|
|
return false
|
|
}
|
|
|
|
splitInstanceType := strings.Split(userInput, ".")
|
|
|
|
if len(splitInstanceType) != 2 {
|
|
return false
|
|
}
|
|
|
|
userDefinedFamily := splitInstanceType[0]
|
|
userDefinedSize := splitInstanceType[1]
|
|
|
|
// Check if instace type has at least 4 vCPUs (= contains "xlarge" in its name)
|
|
hasEnoughVCPUs := strings.Contains(userDefinedSize, "xlarge")
|
|
if !hasEnoughVCPUs {
|
|
return false
|
|
}
|
|
|
|
instances := instancetypes.AWSSNPSupportedInstanceFamilies
|
|
if acceptNonCVM {
|
|
instances = instancetypes.AWSSupportedInstanceFamilies
|
|
}
|
|
|
|
// Now check if the user input is a supported family
|
|
// Note that we cannot directly use the family split from the Graviton check above, as some instances are directly specified by their full name and not just the family in general
|
|
for _, supportedFamily := range instances {
|
|
supportedFamilyLowercase := strings.ToLower(supportedFamily)
|
|
if userDefinedFamily == supportedFamilyLowercase {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func validateNoPlaceholder(fl validator.FieldLevel) bool {
|
|
return len(getPlaceholderEntries(fl.Field().Interface().(measurements.M))) == 0
|
|
}
|
|
|
|
// validateMeasurement acts like validateNoPlaceholder, but is used for the measurements.Measurement type.
|
|
func validateMeasurement(sl validator.StructLevel) {
|
|
measurement := sl.Current().Interface().(measurements.Measurement)
|
|
actual := measurement.Expected
|
|
placeHolder := measurements.PlaceHolderMeasurement(measurements.PCRMeasurementLength).Expected
|
|
if bytes.Equal(actual, placeHolder) {
|
|
sl.ReportError(measurement, "launchMeasurement", "launchMeasurement", "no_placeholders", "")
|
|
}
|
|
}
|
|
|
|
func registerContainsPlaceholderError(ut ut.Translator) error {
|
|
return ut.Add("no_placeholders", "{0} placeholder values (repeated 1234...)", true)
|
|
}
|
|
|
|
func translateContainsPlaceholderError(ut ut.Translator, fe validator.FieldError) string {
|
|
var msg string
|
|
switch fe.Field() {
|
|
case "launchMeasurement":
|
|
msg = "launchMeasurement contains"
|
|
case "measurements":
|
|
placeholders := getPlaceholderEntries(fe.Value().(measurements.M))
|
|
msg = fmt.Sprintf("measurements %v contain", placeholders)
|
|
if len(placeholders) == 1 {
|
|
msg = fmt.Sprintf("measurement %v contains", placeholders)
|
|
}
|
|
}
|
|
|
|
t, _ := ut.T("no_placeholders", msg)
|
|
return t
|
|
}
|
|
|
|
func getPlaceholderEntries(m measurements.M) []uint32 {
|
|
var placeholders []uint32
|
|
placeholderTDX := measurements.PlaceHolderMeasurement(measurements.TDXMeasurementLength)
|
|
placeholderTPM := measurements.PlaceHolderMeasurement(measurements.PCRMeasurementLength)
|
|
|
|
for idx, measurement := range m {
|
|
if bytes.Equal(measurement.Expected, placeholderTDX.Expected) ||
|
|
bytes.Equal(measurement.Expected, placeholderTPM.Expected) {
|
|
placeholders = append(placeholders, idx)
|
|
}
|
|
}
|
|
|
|
return placeholders
|
|
}
|
|
|
|
// validateK8sVersion does not check the patch version.
|
|
func (c *Config) validateK8sVersion(fl validator.FieldLevel) bool {
|
|
_, err := versions.NewValidK8sVersion(compatibility.EnsurePrefixV(fl.Field().String()), false)
|
|
return err == nil
|
|
}
|
|
|
|
func registerImageCompatibilityError(ut ut.Translator) error {
|
|
return ut.Add("image_compatibility", "{0} specifies an invalid version: {1}", true)
|
|
}
|
|
|
|
// Check that the validated field and the CLI version are not more than one minor version apart.
|
|
func validateVersionCompatibility(fl validator.FieldLevel) bool {
|
|
binaryVersion := constants.BinaryVersion()
|
|
if err := validateImageCompatibilityHelper(binaryVersion, fl.FieldName(), fl.Field().String()); err != nil {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func validateImageCompatibilityHelper(binaryVersion consemver.Semver, fieldName, configuredVersion string) error {
|
|
if fieldName == "image" {
|
|
imageVersion, err := versionsapi.NewVersionFromShortPath(configuredVersion, versionsapi.VersionKindImage)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
configuredVersion = imageVersion.Version()
|
|
}
|
|
|
|
return compatibility.BinaryWith(binaryVersion.String(), configuredVersion)
|
|
}
|
|
|
|
func translateImageCompatibilityError(ut ut.Translator, fe validator.FieldError) string {
|
|
binaryVersion := constants.BinaryVersion()
|
|
err := validateImageCompatibilityHelper(binaryVersion, fe.Field(), fe.Value().(string))
|
|
|
|
msg := msgFromCompatibilityError(err, binaryVersion.String(), fe.Value().(string))
|
|
|
|
t, _ := ut.T("image_compatibility", fe.Field(), msg)
|
|
|
|
return t
|
|
}
|
|
|
|
// msgFromCompatibilityError translates compatibility errors into user-facing error messages.
|
|
func msgFromCompatibilityError(err error, binaryVersion, fieldValue string) string {
|
|
switch {
|
|
case errors.Is(err, compatibility.ErrSemVer):
|
|
return fmt.Sprintf("configured version (%s) does not adhere to SemVer syntax", fieldValue)
|
|
case errors.Is(err, compatibility.ErrMajorMismatch):
|
|
return fmt.Sprintf("the CLI's major version (%s) has to match your configured major version (%s). Use --force to ignore the version mismatch.", binaryVersion, fieldValue)
|
|
case errors.Is(err, compatibility.ErrMinorDrift):
|
|
return fmt.Sprintf("the CLI's minor version (%s) and the configured version (%s) are more than one minor version apart. Use --force to ignore the version mismatch.", binaryVersion, fieldValue)
|
|
case errors.Is(err, compatibility.ErrOutdatedCLI):
|
|
return fmt.Sprintf("the CLI's version (%s) is older than the configured version (%s). Use --force to ignore the version mismatch.", binaryVersion, fieldValue)
|
|
default:
|
|
return err.Error()
|
|
}
|
|
}
|
|
|
|
// ValidateMicroserviceVersion checks that the version of the microservice is compatible with the binary version.
|
|
func ValidateMicroserviceVersion(binaryVersion, version consemver.Semver) error {
|
|
// Major versions always have to match.
|
|
if binaryVersion.Major() != version.Major() {
|
|
return compatibility.ErrMajorMismatch
|
|
}
|
|
// Allow newer CLIs (for upgrades), but dissallow newer service versions.
|
|
if binaryVersion.Compare(version) == -1 {
|
|
return compatibility.ErrOutdatedCLI
|
|
}
|
|
// Abort if minor version drift between CLI and versionA value is greater than 1.
|
|
if binaryVersion.Minor()-version.Minor() > 1 {
|
|
return compatibility.ErrMinorDrift
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func returnsTrue(_ validator.FieldLevel) bool {
|
|
return true
|
|
}
|
|
|
|
func (c *Config) registerValidateNameError(ut ut.Translator) error {
|
|
if c.Provider.AWS != nil {
|
|
return ut.Add("validate_name", "{0} must be no more than {1} characters long and contain only lowercase letters", true)
|
|
}
|
|
return ut.Add("validate_name", "{0} must be no more than {1} characters long", true)
|
|
}
|
|
|
|
func (c *Config) translateValidateNameError(ut ut.Translator, fe validator.FieldError) string {
|
|
var t string
|
|
if c.Provider.AWS != nil {
|
|
t, _ = ut.T("validate_name", fe.Field(), strconv.Itoa(constants.AWSConstellationNameLength))
|
|
} else {
|
|
t, _ = ut.T("validate_name", fe.Field(), strconv.Itoa(constants.ConstellationNameLength))
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
// validateName makes sure the name of the constellation is not too long.
|
|
// Since this value may differ between providers, we can't simply use built-in validation.
|
|
// This also allows us to eventually add more validation rules for constellation names if necessary.
|
|
func (c *Config) validateName(fl validator.FieldLevel) bool {
|
|
if c.Provider.AWS != nil {
|
|
name := fl.Field().String()
|
|
return strings.ToLower(name) == name && len(name) <= constants.AWSConstellationNameLength
|
|
}
|
|
return len(fl.Field().String()) <= constants.ConstellationNameLength
|
|
}
|
|
|
|
func warnDeprecated(fl validator.FieldLevel) bool {
|
|
fmt.Fprintf(
|
|
os.Stderr,
|
|
"WARNING: The config key %q is deprecated and will be removed in an upcoming version.\n",
|
|
fl.FieldName(),
|
|
)
|
|
return true
|
|
}
|
|
|
|
func (c *Config) validateNodeGroupZoneField(fl validator.FieldLevel) bool {
|
|
switch c.GetProvider() {
|
|
case cloudprovider.AWS:
|
|
return validateAWSZoneField(fl)
|
|
case cloudprovider.Azure:
|
|
return validateAzureZoneField(fl)
|
|
case cloudprovider.GCP:
|
|
return validateGCPZoneField(fl)
|
|
case cloudprovider.OpenStack:
|
|
return validateOpenStackRegionField(fl)
|
|
case cloudprovider.QEMU:
|
|
// QEMU does not use zones
|
|
return true
|
|
case cloudprovider.Unknown:
|
|
return true
|
|
}
|
|
// this indicates we are missing a case for a new provider
|
|
return false
|
|
}
|
|
|
|
func (c *Config) validateInstanceType(fl validator.FieldLevel) bool {
|
|
return validInstanceTypeForProvider(fl.Field().String(), c.GetAttestationConfig().GetVariant())
|
|
}
|
|
|
|
func (c *Config) validateStateDiskTypeField(fl validator.FieldLevel) bool {
|
|
switch c.GetProvider() {
|
|
case cloudprovider.AWS:
|
|
return validateAWSStateDiskField(fl)
|
|
case cloudprovider.Azure:
|
|
return validateAzureStateDiskField(fl)
|
|
case cloudprovider.GCP:
|
|
return validateGCPStateDiskField(fl)
|
|
case cloudprovider.OpenStack:
|
|
return validateOpenStackStateDiskField(fl)
|
|
case cloudprovider.QEMU:
|
|
return validateQEMUStateDiskField(fl)
|
|
case cloudprovider.Unknown:
|
|
return true
|
|
|
|
}
|
|
// this indicates we are missing a case for a new provider
|
|
return false
|
|
}
|
|
|
|
func validateAWSStateDiskField(fl validator.FieldLevel) bool {
|
|
gotDiskField := fl.Field().String()
|
|
for _, diskType := range disktypes.AWSDiskTypes {
|
|
if gotDiskField == diskType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func validateAzureStateDiskField(fl validator.FieldLevel) bool {
|
|
gotDiskField := fl.Field().String()
|
|
for _, diskType := range disktypes.AzureDiskTypes {
|
|
if gotDiskField == diskType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func validateGCPStateDiskField(fl validator.FieldLevel) bool {
|
|
gotDiskField := fl.Field().String()
|
|
for _, diskType := range disktypes.GCPDiskTypes {
|
|
if gotDiskField == diskType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func validateOpenStackStateDiskField(fl validator.FieldLevel) bool {
|
|
return len(fl.Field().String()) > 0
|
|
}
|
|
|
|
func validateQEMUStateDiskField(_ validator.FieldLevel) bool {
|
|
return true
|
|
}
|