From 15c20d55e01cdcc2fdd91d3d390f6817a88c676f Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Tue, 7 Nov 2023 09:32:57 +0100 Subject: [PATCH 1/4] Cleanup validation errors (#967) * Simplify validation error handling (use native errors) * bump chart versoin to 1.44. * rename errors to match best practice --------- Co-authored-by: Richard Hagen --- charts/radix-operator/Chart.yaml | 4 +- pipeline-runner/steps/apply_radixconfig.go | 6 +- pipeline-runner/steps/promotion.go | 4 +- pkg/apis/radixvalidators/errors.go | 568 +++++++++++-------- pkg/apis/radixvalidators/validate_ra.go | 348 +++++------- pkg/apis/radixvalidators/validate_ra_test.go | 374 ++++++------ pkg/apis/radixvalidators/validate_rd.go | 16 +- pkg/apis/radixvalidators/validate_rd_test.go | 25 +- pkg/apis/radixvalidators/validate_rr.go | 33 +- 9 files changed, 698 insertions(+), 680 deletions(-) diff --git a/charts/radix-operator/Chart.yaml b/charts/radix-operator/Chart.yaml index 0ec28bab9..e63664c01 100644 --- a/charts/radix-operator/Chart.yaml +++ b/charts/radix-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: radix-operator -version: 1.24.0 -appVersion: 1.44.0 +version: 1.24.1 +appVersion: 1.44.1 kubeVersion: ">=1.24.0" description: Radix Operator keywords: diff --git a/pipeline-runner/steps/apply_radixconfig.go b/pipeline-runner/steps/apply_radixconfig.go index f21eaf293..79894d86d 100644 --- a/pipeline-runner/steps/apply_radixconfig.go +++ b/pipeline-runner/steps/apply_radixconfig.go @@ -198,10 +198,10 @@ func CreateRadixApplication(radixClient radixclient.Interface, ra.Name = strings.ToLower(ra.Name) } - isRAValid, errs := validate.CanRadixApplicationBeInsertedErrors(radixClient, ra) - if !isRAValid { + err = validate.CanRadixApplicationBeInserted(radixClient, ra) + if err != nil { log.Errorf("Radix config not valid.") - return nil, errorUtils.Concat(errs) + return nil, err } return ra, nil } diff --git a/pipeline-runner/steps/promotion.go b/pipeline-runner/steps/promotion.go index 7d6b3479b..742285fdf 100644 --- a/pipeline-runner/steps/promotion.go +++ b/pipeline-runner/steps/promotion.go @@ -129,8 +129,8 @@ func (cli *PromoteStepImplementation) Run(pipelineInfo *model.PipelineInfo) erro return err } - isValid, err := radixvalidators.CanRadixDeploymentBeInserted(cli.GetRadixclient(), radixDeployment) - if !isValid { + err = radixvalidators.CanRadixDeploymentBeInserted(radixDeployment) + if err != nil { return err } diff --git a/pkg/apis/radixvalidators/errors.go b/pkg/apis/radixvalidators/errors.go index 812ae9cd1..5606c1189 100644 --- a/pkg/apis/radixvalidators/errors.go +++ b/pkg/apis/radixvalidators/errors.go @@ -1,455 +1,529 @@ package radixvalidators import ( - "errors" "fmt" "strings" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + "github.com/pkg/errors" ) -// MissingPrivateImageHubUsernameError Error when username for private image hubs is not defined -func MissingPrivateImageHubUsernameError(server string) error { - return fmt.Errorf("username is required for private image hub %s", server) -} +var ( + ErrMissingPrivateImageHubUsername = errors.New("missing private image hub username") + ErrEnvForDNSAppAliasNotDefined = errors.New("env for dnsapp alias not defined") + ErrComponentForDNSAppAliasNotDefined = errors.New("component for dnsapp alias not defined") + ErrExternalAliasCannotBeEmpty = errors.New("external alias cannot be empty") + ErrEnvForDNSExternalAliasNotDefined = errors.New("env for dnsexternal alias not defined") + ErrComponentForDNSExternalAliasNotDefined = errors.New("component for dnsexternal alias not defined") + ErrComponentForDNSExternalAliasIsNotMarkedAsPublic = errors.New("component for dnsexternal alias is not marked as public") + ErrEnvironmentReferencedByComponentDoesNotExist = errors.New("environment referenced by component does not exist") + ErrInvalidPortNameLength = errors.New("invalid port name length") + ErrPortSpecificationCannotBeEmptyForComponent = errors.New("port specification cannot be empty for component") + ErrPortNameIsRequiredForPublicComponent = errors.New("port name is required for public component") + ErrMonitoringPortNameIsNotFoundComponent = errors.New("monitoring port name is not found component") + ErrMultipleMatchingPortNames = errors.New("multiple matching port names") + ErrSchedulerPortCannotBeEmptyForJob = errors.New("scheduler port cannot be empty for job") + ErrPayloadPathCannotBeEmptyForJob = errors.New("payload path cannot be empty for job") + ErrMemoryResourceRequirementFormat = errors.New("memory resource requirement format") + ErrCPUResourceRequirementFormat = errors.New("cpuresource requirement format") + ErrInvalidVerificationType = errors.New("invalid verification") + ErrResourceRequestOverLimit = errors.New("resource request over limit") + ErrInvalidResource = errors.New("invalid resource") + ErrDuplicateExternalAlias = errors.New("duplicate external alias") + ErrInvalidBranchName = errors.New("invalid branch name") + ErrMaxReplicasForHPANotSetOrZero = errors.New("max replicas for hpanot set or zero") + ErrMinReplicasGreaterThanMaxReplicas = errors.New("min replicas greater than max replicas") + ErrNoScalingResourceSet = errors.New("no scaling resource set") + ErrEmptyVolumeMountTypeOrDriverSection = errors.New("empty volume mount type or driver section") + ErrMultipleVolumeMountTypesDefined = errors.New("multiple volume mount types defined") + ErrEmptyVolumeMountNameOrPath = errors.New("empty volume mount name or path") + ErrEmptyVolumeMountStorage = errors.New("empty volume mount storage") + ErrEmptyBlobFuse2VolumeMountContainer = errors.New("empty blob fuse 2 volume mount container") + ErrUnsupportedBlobFuse2VolumeMountProtocol = errors.New("unsupported blob fuse 2 volume mount protocol") + ErrDuplicatePathForVolumeMountType = errors.New("duplicate path for volume mount") + ErrDuplicateNameForVolumeMountType = errors.New("duplicate name for volume mount") + ErrunknownVolumeMountType = errors.New("unknown volume mount type") + ErrApplicationNameNotLowercase = errors.New("application name not lowercase") + ErrPublicImageComponentCannotHaveSourceOrDockerfileSet = errors.New("public image component cannot have source or dockerfile") + ErrComponentWithDynamicTagRequiresTagInEnvironmentConfig = errors.New("component with dynamic tag requires tag in environment") + ErrComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment = errors.New("component with dynamic tag requires tag in environment config for") + ErrComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag = errors.New("component with tag in environment config for environment requires dynamic") + ErrComponentNameReservedSuffix = errors.New("component name reserved suffix") + ErrSecretNameConflictsWithEnvironmentVariable = errors.New("secret name conflicts with environment") + ErrInvalidStringValueMaxLength = errors.New("invalid string value max length") + ErrInvalidStringValueMinLength = errors.New("invalid string value min length") + ErrResourceNameCannotBeEmpty = errors.New("resource name cannot be empty") + ErrInvalidUUID = errors.New("invalid") + ErrInvalidEmail = errors.New("invalid email") + ErrInvalidResourceName = errors.New("invalid resource name") + ErrInvalidLowerCaseAlphaNumericDotDashResourceName = errors.New("invalid lower case alpha numeric dot dash resource name") + ErrNoRegistrationExistsForApplication = errors.New("no registration exists for application") + ErrInvalidConfigBranchName = errors.New("invalid config branch") + ErrOauth = errors.New("oauth error") + ErrOAuthClientIdEmpty = errors.Wrap(ErrOauth, "oauth client id empty") + ErrOAuthProxyPrefixEmpty = errors.Wrap(ErrOauth, "oauth proxy prefix empty") + ErrOAuthProxyPrefixIsRoot = errors.Wrap(ErrOauth, "oauth proxy prefix is root") + ErrOAuthSessionStoreTypeInvalid = errors.Wrap(ErrOauth, "oauth session store type invalid") + ErrOAuthOidcJwksUrlEmpty = errors.Wrap(ErrOauth, "oauth oidc jwks url empty") + ErrOAuthLoginUrlEmpty = errors.Wrap(ErrOauth, "oauth login url empty") + ErrOAuthRedeemUrlEmpty = errors.Wrap(ErrOauth, "oauth redeem url empty") + ErrOAuthOidcEmpty = errors.Wrap(ErrOauth, "oauth oidc empty") + ErrOAuthOidcSkipDiscoveryEmpty = errors.Wrap(ErrOauth, "oauth oidc skip discovery empty") + ErrOAuthRedisStoreEmpty = errors.Wrap(ErrOauth, "oauth redis store empty") + ErrOAuthRedisStoreConnectionURLEmpty = errors.Wrap(ErrOauth, "oauth redis store connection urlempty") + ErrOAuthCookieStoreMinimalIncorrectSetXAuthRequestHeaders = errors.Wrap(ErrOauth, "oauth cookie store minimal incorrect set xauth request headers") + ErrOAuthCookieStoreMinimalIncorrectSetAuthorizationHeader = errors.Wrap(ErrOauth, "oauth cookie store minimal incorrect set authorization header") + ErrOAuthCookieStoreMinimalIncorrectCookieRefreshInterval = errors.Wrap(ErrOauth, "oauth cookie store minimal incorrect cookie refresh interval") + ErrOAuthCookieEmpty = errors.Wrap(ErrOauth, "oauth cookie empty") + ErrOAuthCookieNameEmpty = errors.Wrap(ErrOauth, "oauth cookie name empty") + ErrOAuthCookieSameSiteInvalid = errors.Wrap(ErrOauth, "oauth cookie same site invalid") + ErrOAuthCookieExpireInvalid = errors.Wrap(ErrOauth, "oauth cookie expire invalid") + ErrOAuthCookieRefreshInvalid = errors.Wrap(ErrOauth, "oauth cookie refresh invalid") + ErrOAuthCookieRefreshMustBeLessThanExpire = errors.New("oauth cookie refresh must be less than expire") + ErrDuplicateComponentOrJobName = errors.New("duplicate component or job name") + ErrInvalidPortNumber = errors.New("invalid port number") + ErrDuplicateSecretName = errors.New("duplicate secret") + DuplicateEnvVarName = errors.New("duplicate env var") + ErrDuplicateAlias = errors.New("duplicate") + ErrDuplicateAzureKeyVaultName = errors.New("duplicate azure key vault") + ErrSecretRefEnvVarNameConflictsWithEnvironmentVariable = errors.New("secret ref env var name conflicts with environment") + ErrNotValidCidr = errors.New("not valid cidr") + ErrNotValidIPv4Cidr = errors.New("not valid ipv 4 cidr") + ErrInvalidEgressPortProtocol = errors.New("invalid egress port protocol") + ErrWebhook = errors.New("get webhook") + ErrInvalidWebhookUrl = errors.Wrap(ErrWebhook, "invalid webhook") + ErrNotAllowedSchemeInWebhookUrl = errors.Wrap(ErrWebhook, "not allowed scheme in webhook") + ErrMissingPortInWebhookUrl = errors.Wrap(ErrWebhook, "missing port in webhook") + ErrOnlyAppComponentAllowedInWebhookUrl = errors.Wrap(ErrWebhook, "only app component allowed in webhook") + ErrInvalidPortInWebhookUrl = errors.Wrap(ErrWebhook, "invalid port in webhook") + ErrInvalidUseOfPublicPortInWebhookUrl = errors.New("invalid use of public port in webhook") + ErrMissingAzureIdentity = errors.New("missing identity") +) -// EnvForDNSAppAliasNotDefinedError Error when env not defined -func EnvForDNSAppAliasNotDefinedError(env string) error { - return fmt.Errorf("env %s referred to by dnsAppAlias is not defined", env) +// MissingPrivateImageHubUsernameErrorWithMessage Error when username for private image hubs is not defined +func MissingPrivateImageHubUsernameErrorWithMessage(server string) error { + return errors.WithMessage(ErrMissingPrivateImageHubUsername, server) } -// ComponentForDNSAppAliasNotDefinedError Error when env not defined -func ComponentForDNSAppAliasNotDefinedError(component string) error { - return fmt.Errorf("component %s referred to by dnsAppAlias is not defined", component) +// EnvForDNSAppAliasNotDefinedErrorWithMessage Error when env not defined +func EnvForDNSAppAliasNotDefinedErrorWithMessage(env string) error { + return errors.WithMessagef(ErrEnvForDNSAppAliasNotDefined, "env %s referred to by dnsAppAlias is not defined", env) } -// ExternalAliasCannotBeEmptyError Structure cannot be left empty -func ExternalAliasCannotBeEmptyError() error { - return errors.New("external alias cannot be empty") +// ComponentForDNSAppAliasNotDefinedErrorWithMessage Error when env not defined +func ComponentForDNSAppAliasNotDefinedErrorWithMessage(component string) error { + return errors.WithMessagef(ErrComponentForDNSAppAliasNotDefined, "component %s referred to by dnsAppAlias is not defined", component) } -// EnvForDNSExternalAliasNotDefinedError Error when env not defined -func EnvForDNSExternalAliasNotDefinedError(env string) error { - return fmt.Errorf("env %s referred to by dnsExternalAlias is not defined", env) +// EnvForDNSExternalAliasNotDefinedErrorWithMessage Error when env not defined +func EnvForDNSExternalAliasNotDefinedErrorWithMessage(env string) error { + return errors.WithMessagef(ErrEnvForDNSExternalAliasNotDefined, "env %s referred to by dnsExternalAlias is not defined", env) } -// ComponentForDNSExternalAliasNotDefinedError Error when env not defined -func ComponentForDNSExternalAliasNotDefinedError(component string) error { - return fmt.Errorf("component %s referred to by dnsExternalAlias is not defined", component) +// ComponentForDNSExternalAliasNotDefinedErrorWithMessage Error when env not defined +func ComponentForDNSExternalAliasNotDefinedErrorWithMessage(component string) error { + return errors.WithMessagef(ErrComponentForDNSExternalAliasNotDefined, "component %s referred to by dnsExternalAlias is not defined", component) } -// ComponentForDNSExternalAliasIsNotMarkedAsPublicError Component is not marked as public -func ComponentForDNSExternalAliasIsNotMarkedAsPublicError(component string) error { - return fmt.Errorf("component %s referred to by dnsExternalAlias is not marked as public", component) +// ComponentForDNSExternalAliasIsNotMarkedAsPublicErrorWithMessage Component is not marked as public +func ComponentForDNSExternalAliasIsNotMarkedAsPublicErrorWithMessage(component string) error { + return errors.WithMessagef(ErrComponentForDNSExternalAliasIsNotMarkedAsPublic, "component %s referred to by dnsExternalAlias is not marked as public", component) } -// EnvironmentReferencedByComponentDoesNotExistError Environment does not exists -func EnvironmentReferencedByComponentDoesNotExistError(environment, component string) error { - return fmt.Errorf("env %s refered to by component %s is not defined", environment, component) +// EnvironmentReferencedByComponentDoesNotExistErrorWithMessage Environment does not exists +func EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(environment, component string) error { + return errors.WithMessagef(ErrEnvironmentReferencedByComponentDoesNotExist, "env %s refered to by component %s is not defined", environment, component) } -// InvalidPortNameLengthError Invalid resource length -func InvalidPortNameLengthError(value string) error { - return fmt.Errorf("%s (%s) max length is %d", "port name", value, maxPortNameLength) +// InvalidPortNameLengthErrorWithMessage Invalid resource length +func InvalidPortNameLengthErrorWithMessage(value string) error { + return errors.WithMessagef(ErrInvalidPortNameLength, "%s (%s) max length is %d", "port name", value, maxPortNameLength) } -// PortSpecificationCannotBeEmptyForComponentError Port cannot be empty for component -func PortSpecificationCannotBeEmptyForComponentError(component string) error { - return fmt.Errorf("port specification cannot be empty for %s", component) +// PortSpecificationCannotBeEmptyForComponentErrorWithMessage Port cannot be empty for component +func PortSpecificationCannotBeEmptyForComponentErrorWithMessage(component string) error { + return errors.WithMessagef(ErrPortSpecificationCannotBeEmptyForComponent, "port specification cannot be empty for %s", component) } -// PortNameIsRequiredForPublicComponentError Port name cannot be empty -func PortNameIsRequiredForPublicComponentError(publicPortName, component string) error { - return fmt.Errorf("%s port name is required for public component %s", publicPortName, component) +// PortNameIsRequiredForPublicComponentErrorWithMessage Port name cannot be empty +func PortNameIsRequiredForPublicComponentErrorWithMessage(publicPortName, component string) error { + return errors.WithMessagef(ErrPortNameIsRequiredForPublicComponent, "%s port name is required for public component %s", publicPortName, component) } -// MonitoringPortNameIsNotFoundComponentError Monitoring port name not found on component -func MonitoringPortNameIsNotFoundComponentError(portName, component string) error { - return fmt.Errorf("%s port name referred to in MonitoringConfig not found on component %s", portName, component) +// MonitoringPortNameIsNotFoundComponentErrorWithMessage Monitoring port name not found on component +func MonitoringPortNameIsNotFoundComponentErrorWithMessage(portName, component string) error { + return errors.WithMessagef(ErrMonitoringPortNameIsNotFoundComponent, "%s port name referred to in MonitoringConfig not found on component %s", portName, component) } -// MultipleMatchingPortNamesError Multiple matching port names -func MultipleMatchingPortNamesError(matchingPortName int, publicPortName, component string) error { - return fmt.Errorf("there are %d ports with name %s for component %s. Only 1 is allowed", matchingPortName, publicPortName, component) +// MultipleMatchingPortNamesErrorWithMessage Multiple matching port names +func MultipleMatchingPortNamesErrorWithMessage(matchingPortName int, publicPortName, component string) error { + return errors.WithMessagef(ErrMultipleMatchingPortNames, "there are %d ports with name %s for component %s. Only 1 is allowed", matchingPortName, publicPortName, component) } -// SchedulerPortCannotBeEmptyForJobError Scheduler port cannot be empty for job -func SchedulerPortCannotBeEmptyForJobError(jobName string) error { - return fmt.Errorf("scheduler port cannot be empty for %s", jobName) +// SchedulerPortCannotBeEmptyForJobErrorWithMessage Scheduler port cannot be empty for job +func SchedulerPortCannotBeEmptyForJobErrorWithMessage(jobName string) error { + return errors.WithMessagef(ErrSchedulerPortCannotBeEmptyForJob, "scheduler port cannot be empty for %s", jobName) } -// PayloadPathCannotBeEmptyForJobError Payload path cannot be empty for job -func PayloadPathCannotBeEmptyForJobError(jobName string) error { - return fmt.Errorf("payload path cannot be empty for %s", jobName) +// PayloadPathCannotBeEmptyForJobErrorWithMessage Payload path cannot be empty for job +func PayloadPathCannotBeEmptyForJobErrorWithMessage(jobName string) error { + return errors.WithMessagef(ErrPayloadPathCannotBeEmptyForJob, "payload path cannot be empty for %s", jobName) } -// MemoryResourceRequirementFormatError Invalid memory resource requirement error -func MemoryResourceRequirementFormatError(value string) error { - return fmt.Errorf("format of memory resource requirement %s (value %s) is wrong. Value must be a valid Kubernetes quantity", "memory", value) +// MemoryResourceRequirementFormatErrorWithMessage Invalid memory resource requirement error +func MemoryResourceRequirementFormatErrorWithMessage(value string) error { + return errors.WithMessagef(ErrMemoryResourceRequirementFormat, "format of memory resource requirement %s (value %s) is wrong. Value must be a valid Kubernetes quantity", "memory", value) } -// CPUResourceRequirementFormatError Invalid CPU resource requirement -func CPUResourceRequirementFormatError(value string) error { - return fmt.Errorf("format of cpu resource requirement %s (value %s) is wrong. Must match regex %s", "cpu", value, cpuRegex) +// CPUResourceRequirementFormatErrorWithMessage Invalid CPU resource requirement +func CPUResourceRequirementFormatErrorWithMessage(value string) error { + return errors.WithMessagef(ErrCPUResourceRequirementFormat, "format of cpu resource requirement %s (value %s) is wrong. Must match regex %s", "cpu", value, cpuRegex) } -func InvalidVerificationType(verification string) error { - return fmt.Errorf("invalid VerificationType (value %s)", verification) +func InvalidVerificationTypeWithMessage(verification string) error { + return errors.WithMessagef(ErrInvalidVerificationType, "invalid VerificationType (value %s)", verification) } -// ResourceRequestOverLimitError Invalid resource requirement error -func ResourceRequestOverLimitError(resource string, require string, limit string) error { - return fmt.Errorf("%s resource requirement (value %s) is larger than the limit (value %s)", resource, require, limit) +// ResourceRequestOverLimitErrorWithMessage Invalid resource requirement error +func ResourceRequestOverLimitErrorWithMessage(resource string, require string, limit string) error { + return errors.WithMessagef(ErrResourceRequestOverLimit, "%s resource requirement (value %s) is larger than the limit (value %s)", resource, require, limit) } -// InvalidResourceError Invalid resource type -func InvalidResourceError(name string) error { - return fmt.Errorf("only support resource requirement type 'memory' and 'cpu' (not %s)", name) +// InvalidResourceErrorWithMessage Invalid resource type +func InvalidResourceErrorWithMessage(name string) error { + return errors.WithMessagef(ErrInvalidResource, "only support resource requirement type 'memory' and 'cpu' (not %s)", name) } -// DuplicateExternalAliasError Cannot have duplicate external alias -func DuplicateExternalAliasError() error { - return errors.New("cannot have duplicate aliases for dnsExternalAlias") +// DuplicateExternalAliasErrorWithMessage Cannot have duplicate external alias +func DuplicateExternalAliasErrorWithMessage() error { + return errors.WithMessage(ErrDuplicateExternalAlias, "cannot have duplicate aliases for dnsExternalAlias") } -// InvalidBranchNameError Indicates that branch name is invalid -func InvalidBranchNameError(branch string) error { - return fmt.Errorf("invalid branch name %s. See documentation for more info", branch) +// InvalidBranchNameErrorWithMessage Indicates that branch name is invalid +func InvalidBranchNameErrorWithMessage(branch string) error { + return errors.WithMessagef(ErrInvalidBranchName, "invalid branch name %s. See documentation for more info", branch) } -// MaxReplicasForHPANotSetOrZeroError Indicates that minReplicas of horizontalScaling is not set or set to 0 -func MaxReplicasForHPANotSetOrZeroError(component, environment string) error { - return fmt.Errorf("maxReplicas is not set or set to 0 for component %s in environment %s. See documentation for more info", component, environment) +// MaxReplicasForHPANotSetOrZeroErrorWithMessage Indicates that minReplicas of horizontalScaling is not set or set to 0 +func MaxReplicasForHPANotSetOrZeroErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrMaxReplicasForHPANotSetOrZero, "maxReplicas is not set or set to 0 for component %s in environment %s. See documentation for more info", component, environment) } -// MinReplicasGreaterThanMaxReplicasError Indicates that minReplicas is greater than maxReplicas -func MinReplicasGreaterThanMaxReplicasError(component, environment string) error { - return fmt.Errorf("minReplicas is greater than maxReplicas for component %s in environment %s. See documentation for more info", component, environment) +// MinReplicasGreaterThanMaxReplicasErrorWithMessage Indicates that minReplicas is greater than maxReplicas +func MinReplicasGreaterThanMaxReplicasErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrMinReplicasGreaterThanMaxReplicas, "minReplicas is greater than maxReplicas for component %s in environment %s. See documentation for more info", component, environment) } -// NoScalingResourceSetError Indicates that no scaling resource is set for horizontal scaling -func NoScalingResourceSetError(component, environment string) error { - return fmt.Errorf("no scaling resource is set for component %s in environment %s. See documentation for more info", component, environment) +// NoScalingResourceSetErrorWithMessage Indicates that no scaling resource is set for horizontal scaling +func NoScalingResourceSetErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrNoScalingResourceSet, "no scaling resource is set for component %s in environment %s. See documentation for more info", component, environment) } -func emptyVolumeMountTypeOrDriverSectionError(component, environment string) error { - return fmt.Errorf("non of volume mount type, blobfuse2 or azureFile options are defined in the volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func emptyVolumeMountTypeOrDriverSectionErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrEmptyVolumeMountTypeOrDriverSection, "non of volume mount type, blobfuse2 or azureFile options are defined in the volumeMount for component %s in environment %s. See documentation for more info", component, environment) } -func multipleVolumeMountTypesDefinedError(component, environment string) error { - return fmt.Errorf("multiple volume mount types defined in the volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func multipleVolumeMountTypesDefinedErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrMultipleVolumeMountTypesDefined, "multiple volume mount types defined in the volumeMount for component %s in environment %s. See documentation for more info", component, environment) } -func emptyVolumeMountNameOrPathError(component, environment string) error { - return fmt.Errorf("missing volume mount name and path of volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func emptyVolumeMountNameOrPathErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrEmptyVolumeMountNameOrPath, "missing volume mount name and path of volumeMount for component %s in environment %s. See documentation for more info", component, environment) } -func emptyVolumeMountStorageError(component, environment string) error { - return fmt.Errorf("missing volume mount storage of volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func emptyVolumeMountStorageErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrEmptyVolumeMountStorage, "missing volume mount storage of volumeMount for component %s in environment %s. See documentation for more info", component, environment) } -func emptyBlobFuse2VolumeMountContainerError(component, environment string) error { - return fmt.Errorf("missing BlobFuse2 volume mount container of volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func emptyBlobFuse2VolumeMountContainerErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrEmptyBlobFuse2VolumeMountContainer, "missing BlobFuse2 volume mount container of volumeMount for component %s in environment %s. See documentation for more info", component, environment) } -func unsupportedBlobFuse2VolumeMountProtocolError(component, environment string) error { - return fmt.Errorf("unsupported BlobFuse2 volume mount protocol of volumeMount for component %s in environment %s. See documentation for more info", component, environment) +func unsupportedBlobFuse2VolumeMountProtocolErrorWithMessage(component, environment string) error { + return errors.WithMessagef(ErrUnsupportedBlobFuse2VolumeMountProtocol, "unsupported BlobFuse2 volume mount protocol of volumeMount for component %s in environment %s. See documentation for more info", component, environment) } -func duplicatePathForVolumeMountType(path, volumeMountType, component, environment string) error { - return fmt.Errorf("duplicate path %s for volume mount type %s, for component %s in environment %s. See documentation for more info", +func duplicatePathForVolumeMountTypeWithMessage(path, volumeMountType, component, environment string) error { + return errors.WithMessagef(ErrDuplicatePathForVolumeMountType, "duplicate path %s for volume mount type %s, for component %s in environment %s. See documentation for more info", path, volumeMountType, component, environment) } -func duplicateNameForVolumeMountType(name, volumeMountType, component, environment string) error { - return fmt.Errorf("duplicate names %s for volume mount type %s, for component %s in environment %s. See documentation for more info", +func duplicateNameForVolumeMountTypeWithMessage(name, volumeMountType, component, environment string) error { + return errors.WithMessagef(ErrDuplicateNameForVolumeMountType, "duplicate names %s for volume mount type %s, for component %s in environment %s. See documentation for more info", name, volumeMountType, component, environment) } -func unknownVolumeMountTypeError(volumeMountType, component, environment string) error { - return fmt.Errorf("not recognized volume mount type %s for component %s in environment %s. See documentation for more info", +func unknownVolumeMountTypeErrorWithMessage(volumeMountType, component, environment string) error { + return errors.WithMessagef(ErrunknownVolumeMountType, "not recognized volume mount type %s for component %s in environment %s. See documentation for more info", volumeMountType, component, environment) } -// ApplicationNameNotLowercaseError Indicates that application name contains upper case letters -func ApplicationNameNotLowercaseError(appName string) error { - return fmt.Errorf("application with name %s contains uppercase letters", appName) +// ApplicationNameNotLowercaseErrorWithMessage Indicates that application name contains upper case letters +func ApplicationNameNotLowercaseErrorWithMessage(appName string) error { + return errors.WithMessagef(ErrApplicationNameNotLowercase, "application with name %s contains uppercase letters", appName) } -// PublicImageComponentCannotHaveSourceOrDockerfileSet Error if image is set and radix config contains src or dockerfile -func PublicImageComponentCannotHaveSourceOrDockerfileSet(componentName string) error { - return fmt.Errorf("component %s cannot have neither 'src' nor 'Dockerfile' set", componentName) +// PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage Error if image is set and radix config contains src or dockerfile +func PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(componentName string) error { + return errors.WithMessagef(ErrPublicImageComponentCannotHaveSourceOrDockerfileSet, "component %s cannot have neither 'src' nor 'Dockerfile' set", componentName) } -// ComponentWithDynamicTagRequiresTagInEnvironmentConfig Error if image is set with dynamic tag and tag is missing -func ComponentWithDynamicTagRequiresTagInEnvironmentConfig(componentName string) error { - return fmt.Errorf("component %s with %s on image requires an image tag set on environment config", +// ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage Error if image is set with dynamic tag and tag is missing +func ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage(componentName string) error { + return errors.WithMessagef(ErrComponentWithDynamicTagRequiresTagInEnvironmentConfig, "component %s with %s on image requires an image tag set on environment config", componentName, radixv1.DynamicTagNameInEnvironmentConfig) } -// ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment Error if image is set with dynamic tag and tag is missing -func ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment(componentName, environment string) error { - return fmt.Errorf( - "component %s with %s on image requires an image tag set on environment config for environment %s", - componentName, radixv1.DynamicTagNameInEnvironmentConfig, environment) -} - -// ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag If tag is set then the dynamic tag needs to be set on the image -func ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag(componentName, environment string) error { - return fmt.Errorf( - "component %s with image tag set on environment config for environment %s requires %s on image setting", - componentName, environment, radixv1.DynamicTagNameInEnvironmentConfig) +// ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironmentWithMessage Error if image is set with dynamic tag and tag is missing +func ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironmentWithMessage(componentName, environment string) error { + return errors.WithMessagef(ErrComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment, "component %s with %s on image requires an image tag set on environment config for environment %s", componentName, radixv1.DynamicTagNameInEnvironmentConfig, environment) } -// ComponentWithDynamicTagRequiresTagInEnvironmentConfig Error if image is set with dynamic tag and tag is missing -func ComponentNameReservedSuffixError(componentName, componentType, suffix string) error { - return fmt.Errorf("%s %s using reserved suffix %s", componentType, componentName, suffix) +// ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage If tag is set then the dynamic tag needs to be set on the image +func ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(componentName, environment string) error { + return errors.WithMessagef(ErrComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag, "component %s with image tag set on environment config for environment %s requires %s on image setting", componentName, environment, radixv1.DynamicTagNameInEnvironmentConfig) } -// SecretNameConflictsWithEnvironmentVariable If secret name is the same as environment variable fail validation -func SecretNameConflictsWithEnvironmentVariable(componentName, secretName string) error { - return fmt.Errorf( - "component %s has a secret with name %s which exists as an environment variable", - componentName, secretName) +// ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage Error if image is set with dynamic tag and tag is missing +func ComponentNameReservedSuffixErrorWithMessage(componentName, componentType, suffix string) error { + return errors.WithMessagef(ErrComponentNameReservedSuffix, "%s %s using reserved suffix %s", componentType, componentName, suffix) } -// InvalidAppNameLengthError Invalid app length -func InvalidAppNameLengthError(value string) error { - return InvalidStringValueMaxLengthError("app name", value, 253) +// SecretNameConflictsWithEnvironmentVariableWithMessage If secret name is the same as environment variable fail validation +func SecretNameConflictsWithEnvironmentVariableWithMessage(componentName, secretName string) error { + return errors.WithMessagef(ErrSecretNameConflictsWithEnvironmentVariable, "component %s has a secret with name %s which exists as an environment variable", componentName, secretName) } -// AppNameCannotBeEmptyError App name cannot be empty -func AppNameCannotBeEmptyError() error { - return ResourceNameCannotBeEmptyError("app name") +// InvalidAppNameLengthErrorWithMessage Invalid app length +func InvalidAppNameLengthErrorWithMessage(value string) error { + return InvalidStringValueMaxLengthErrorWithMessage("app name", value, 253) } -// InvalidStringValueMinLengthError Invalid string value min length -func InvalidStringValueMinLengthError(resourceName, value string, minValue int) error { - return fmt.Errorf("%s (\"%s\") min length is %d", resourceName, value, minValue) +// InvalidStringValueMinLengthErrorWithMessage Invalid string value min length +func InvalidStringValueMinLengthErrorWithMessage(resourceName, value string, minValue int) error { + return errors.WithMessagef(ErrInvalidStringValueMinLength, "%s (\"%s\") min length is %d", resourceName, value, minValue) } -// InvalidStringValueMaxLengthError Invalid string value max length -func InvalidStringValueMaxLengthError(resourceName, value string, maxValue int) error { - return fmt.Errorf("%s (\"%s\") max length is %d", resourceName, value, maxValue) +// InvalidStringValueMaxLengthErrorWithMessage Invalid string value max length +func InvalidStringValueMaxLengthErrorWithMessage(resourceName, value string, maxValue int) error { + return errors.WithMessagef(ErrInvalidStringValueMaxLength, "%s (\"%s\") max length is %d", resourceName, value, maxValue) } -// ResourceNameCannotBeEmptyError Resource name cannot be left empty -func ResourceNameCannotBeEmptyError(resourceName string) error { - return fmt.Errorf("%s cannot be empty", resourceName) +// ResourceNameCannotBeEmptyErrorWithMessage Resource name cannot be left empty +func ResourceNameCannotBeEmptyErrorWithMessage(resourceName string) error { + return errors.WithMessagef(ErrResourceNameCannotBeEmpty, "%s cannot be empty", resourceName) } -// InvalidUUIDError Invalid UUID -func InvalidUUIDError(resourceName string, uuid string) error { - return fmt.Errorf("field %s does not contain a valid UUID (value: %s)", resourceName, uuid) +// InvalidUUIDErrorWithMessage Invalid UUID +func InvalidUUIDErrorWithMessage(resourceName string, uuid string) error { + return errors.WithMessagef(ErrInvalidUUID, "field %s does not contain a valid UUID (value: %s)", resourceName, uuid) } -// InvalidEmailError Invalid email -func InvalidEmailError(resourceName, email string) error { - return fmt.Errorf("field %s does not contain a valid email (value: %s)", resourceName, email) +// InvalidEmailErrorWithMessage Invalid email +func InvalidEmailErrorWithMessage(resourceName, email string) error { + return errors.WithMessagef(ErrInvalidEmail, "field %s does not contain a valid email (value: %s)", resourceName, email) } -// InvalidResourceNameError Invalid resource name -func InvalidResourceNameError(resourceName, value string) error { - return fmt.Errorf("%s %s can only consist of alphanumeric characters, '.' and '-'", resourceName, value) +// InvalidResourceNameErrorWithMessage Invalid resource name +func InvalidResourceNameErrorWithMessage(resourceName, value string) error { + return errors.WithMessagef(ErrInvalidResourceName, "%s %s can only consist of alphanumeric characters, '.' and '-'", resourceName, value) } -// InvalidLowerCaseAlphaNumericDotDashResourceNameError Invalid lower case alpha-numeric, dot, dash resource name error -func InvalidLowerCaseAlphaNumericDotDashResourceNameError(resourceName, value string) error { - return fmt.Errorf("%s %s can only consist of lower case alphanumeric characters, '.' and '-'", resourceName, value) +// InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage Invalid lower case alpha-numeric, dot, dash resource name error +func InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage(resourceName, value string) error { + return errors.WithMessagef(ErrInvalidLowerCaseAlphaNumericDotDashResourceName, "%s %s can only consist of lower case alphanumeric characters, '.' and '-'", resourceName, value) } -// NoRegistrationExistsForApplicationError No registration exists -func NoRegistrationExistsForApplicationError(appName string) error { - return fmt.Errorf("no application found with name %s. Name of the application in radixconfig.yaml needs to be exactly the same as used when defining the app in the console", appName) +// NoRegistrationExistsForApplicationErrorWithMessage No registration exists +func NoRegistrationExistsForApplicationErrorWithMessage(appName string) error { + return errors.WithMessagef(ErrNoRegistrationExistsForApplication, "no application found with name %s. Name of the application in radixconfig.yaml needs to be exactly the same as used when defining the app in the console", appName) } -func InvalidConfigBranchName(configBranch string) error { - return fmt.Errorf("config branch name is not valid (value: %s)", configBranch) +func InvalidConfigBranchNameWithMessage(configBranch string) error { + return errors.WithMessagef(ErrInvalidConfigBranchName, "config branch name is not valid (value: %s)", configBranch) } // *********** OAuth2 config errors *********** -func oauthRequiredPropertyEmptyError(componentName, environmentName, propertyName string) error { - return fmt.Errorf("component %s in environment %s: required property %s in oauth2 configuration not set", componentName, environmentName, propertyName) +func oauthRequiredPropertyEmptyErrorWithMessage(err error, componentName, environmentName, propertyName string) error { + return errors.WithMessagef(err, "component %s in environment %s: required property %s in oauth2 configuration not set", componentName, environmentName, propertyName) } -func oauthPropertyInvalidValueError(componentName, environmentName, propertyName, value string) error { - return fmt.Errorf("component %s in environment %s: invalid value %s for property %s in oauth2 configuration", componentName, environmentName, value, propertyName) +func oauthPropertyInvalidValueErrorWithMessage(err error, componentName, environmentName, propertyName, value string) error { + return errors.WithMessagef(err, "component %s in environment %s: invalid value %s for property %s in oauth2 configuration", componentName, environmentName, value, propertyName) } -func oauthRequiredPropertyEmptyWithConditionError(componentName, environmentName, propertyName, whenCondition string) error { - return fmt.Errorf("component %s in environment %s: property %s in oauth2 configuration must be set when %s", componentName, environmentName, propertyName, whenCondition) +func oauthRequiredPropertyEmptyWithConditionErrorWithMessage(err error, componentName, environmentName, propertyName, whenCondition string) error { + return errors.WithMessagef(err, "component %s in environment %s: property %s in oauth2 configuration must be set when %s", componentName, environmentName, propertyName, whenCondition) } -func oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledError(componentName, environmentName, propertyName string) error { - return oauthRequiredPropertyEmptyWithConditionError(componentName, environmentName, propertyName, "oidc.skipDiscovery is true") +func oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledErrorWithMessage(err error, componentName, environmentName, propertyName string) error { + return oauthRequiredPropertyEmptyWithConditionErrorWithMessage(err, componentName, environmentName, propertyName, "oidc.skipDiscovery is true") } -func oauthRequiredPropertyEmptyWithSessionStoreRedisError(componentName, environmentName, propertyName string) error { - return oauthRequiredPropertyEmptyWithConditionError(componentName, environmentName, propertyName, "sessionStoreType is redis") +func oauthRequiredPropertyEmptyWithSessionStoreRedisErrorWithMessage(err error, componentName, environmentName, propertyName string) error { + return oauthRequiredPropertyEmptyWithConditionErrorWithMessage(err, componentName, environmentName, propertyName, "sessionStoreType is redis") } -func oauthCookieStoreMinimalIncorrectPropertyValueError(componentName, environmentName, propertyName, expectedValue string) error { - return fmt.Errorf("component %s in environment %s: property %s in oauth configuration must be set to %s when cookieStore.minimal is true", componentName, environmentName, propertyName, expectedValue) +func oauthCookieStoreMinimalIncorrectPropertyValueErrorWithMessage(err error, componentName, environmentName, propertyName, expectedValue string) error { + return errors.WithMessagef(err, "component %s in environment %s: property %s in oauth configuration must be set to %s when cookieStore.minimal is true", componentName, environmentName, propertyName, expectedValue) } -func OAuthClientIdEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyError(componentName, environmentName, "clientId") +func OAuthClientIdEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyErrorWithMessage(ErrOAuthClientIdEmpty, componentName, environmentName, "clientId") } -func OAuthProxyPrefixEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyError(componentName, environmentName, "proxyPrefix") +func OAuthProxyPrefixEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyErrorWithMessage(ErrOAuthProxyPrefixEmpty, componentName, environmentName, "proxyPrefix") } -func OAuthProxyPrefixIsRootError(componentName, environmentName string) error { - return fmt.Errorf("component %s in environment %s: property proxyPrefix in oauth configuration cannot be set to '/' (root)", componentName, environmentName) +func OAuthProxyPrefixIsRootErrorWithMessage(componentName, environmentName string) error { + return errors.WithMessagef(ErrOAuthProxyPrefixIsRoot, "component %s in environment %s: property proxyPrefix in oauth configuration cannot be set to '/' (root)", componentName, environmentName) } -func OAuthSessionStoreTypeInvalidError(componentName, environmentName string, actualSessionStoreType radixv1.SessionStoreType) error { - return oauthPropertyInvalidValueError(componentName, environmentName, "sessionStoreType", string(actualSessionStoreType)) +func OAuthSessionStoreTypeInvalidErrorWithMessage(componentName, environmentName string, actualSessionStoreType radixv1.SessionStoreType) error { + return oauthPropertyInvalidValueErrorWithMessage(ErrOAuthSessionStoreTypeInvalid, componentName, environmentName, "sessionStoreType", string(actualSessionStoreType)) } -func OAuthOidcJwksUrlEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledError(componentName, environmentName, "oidc.jwksUrl") +func OAuthOidcJwksUrlEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledErrorWithMessage(ErrOAuthOidcJwksUrlEmpty, componentName, environmentName, "oidc.jwksUrl") } -func OAuthLoginUrlEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledError(componentName, environmentName, "oidc.loginUrl") +func OAuthLoginUrlEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledErrorWithMessage(ErrOAuthLoginUrlEmpty, componentName, environmentName, "oidc.loginUrl") } -func OAuthRedeemUrlEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledError(componentName, environmentName, "oidc.redeemUrl") +func OAuthRedeemUrlEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyWithSkipDiscoveryEnabledErrorWithMessage(ErrOAuthRedeemUrlEmpty, componentName, environmentName, "oidc.redeemUrl") } -func OAuthOidcEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyError(componentName, environmentName, "oidc") +func OAuthOidcEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyErrorWithMessage(ErrOAuthOidcEmpty, componentName, environmentName, "oidc") } -func OAuthOidcSkipDiscoveryEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyError(componentName, environmentName, "oidc.skipDiscovery") +func OAuthOidcSkipDiscoveryEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyErrorWithMessage(ErrOAuthOidcSkipDiscoveryEmpty, componentName, environmentName, "oidc.skipDiscovery") } -func OAuthRedisStoreEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyWithSessionStoreRedisError(componentName, environmentName, "redisStore") +func OAuthRedisStoreEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyWithSessionStoreRedisErrorWithMessage(ErrOAuthRedisStoreEmpty, componentName, environmentName, "redisStore") } -func OAuthRedisStoreConnectionURLEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyWithSessionStoreRedisError(componentName, environmentName, "redisStore.connectionUrl") +func OAuthRedisStoreConnectionURLEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyWithSessionStoreRedisErrorWithMessage(ErrOAuthRedisStoreConnectionURLEmpty, componentName, environmentName, "redisStore.connectionUrl") } -func OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersError(componentName, environmentName string) error { - return oauthCookieStoreMinimalIncorrectPropertyValueError(componentName, environmentName, "setXAuthRequestHeaders", "false") +func OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersErrorWithMessage(componentName, environmentName string) error { + return oauthCookieStoreMinimalIncorrectPropertyValueErrorWithMessage(ErrOAuthCookieStoreMinimalIncorrectSetXAuthRequestHeaders, componentName, environmentName, "setXAuthRequestHeaders", "false") } -func OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderError(componentName, environmentName string) error { - return oauthCookieStoreMinimalIncorrectPropertyValueError(componentName, environmentName, "setAuthorizationHeader", "false") +func OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderErrorWithMessage(componentName, environmentName string) error { + return oauthCookieStoreMinimalIncorrectPropertyValueErrorWithMessage(ErrOAuthCookieStoreMinimalIncorrectSetAuthorizationHeader, componentName, environmentName, "setAuthorizationHeader", "false") } -func OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalError(componentName, environmentName string) error { - return oauthCookieStoreMinimalIncorrectPropertyValueError(componentName, environmentName, "cookie.refresh", "0") +func OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalErrorWithMessage(componentName, environmentName string) error { + return oauthCookieStoreMinimalIncorrectPropertyValueErrorWithMessage(ErrOAuthCookieStoreMinimalIncorrectCookieRefreshInterval, componentName, environmentName, "cookie.refresh", "0") } -func OAuthCookieEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyError(componentName, environmentName, "cookie") +func OAuthCookieEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyErrorWithMessage(ErrOAuthCookieEmpty, componentName, environmentName, "cookie") } -func OAuthCookieNameEmptyError(componentName, environmentName string) error { - return oauthRequiredPropertyEmptyError(componentName, environmentName, "cookie.name") +func OAuthCookieNameEmptyErrorWithMessage(componentName, environmentName string) error { + return oauthRequiredPropertyEmptyErrorWithMessage(ErrOAuthCookieNameEmpty, componentName, environmentName, "cookie.name") } -func OAuthCookieSameSiteInvalidError(componentName, environmentName string, actualSameSite radixv1.CookieSameSiteType) error { - return oauthPropertyInvalidValueError(componentName, environmentName, "cookie.sameSite", string(actualSameSite)) +func OAuthCookieSameSiteInvalidErrorWithMessage(componentName, environmentName string, actualSameSite radixv1.CookieSameSiteType) error { + return oauthPropertyInvalidValueErrorWithMessage(ErrOAuthCookieSameSiteInvalid, componentName, environmentName, "cookie.sameSite", string(actualSameSite)) } -func OAuthCookieExpireInvalidError(componentName, environmentName, actualExpire string) error { - return oauthPropertyInvalidValueError(componentName, environmentName, "cookie.expire", actualExpire) +func OAuthCookieExpireInvalidErrorWithMessage(componentName, environmentName, actualExpire string) error { + return oauthPropertyInvalidValueErrorWithMessage(ErrOAuthCookieExpireInvalid, componentName, environmentName, "cookie.expire", actualExpire) } -func OAuthCookieRefreshInvalidError(componentName, environmentName, actualRefresh string) error { - return oauthPropertyInvalidValueError(componentName, environmentName, "cookie.refresh", actualRefresh) +func OAuthCookieRefreshInvalidErrorWithMessage(componentName, environmentName, actualRefresh string) error { + return oauthPropertyInvalidValueErrorWithMessage(ErrOAuthCookieRefreshInvalid, componentName, environmentName, "cookie.refresh", actualRefresh) } -func OAuthCookieRefreshMustBeLessThanExpireError(componentName, environmentName string) error { - return fmt.Errorf("component %s in environment %s: property cookie.refresh in oauth configuration must be less than cookie.expire", componentName, environmentName) +func OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(componentName, environmentName string) error { + return errors.WithMessagef(ErrOAuthCookieRefreshMustBeLessThanExpire, "component %s in environment %s: property cookie.refresh in oauth configuration must be less than cookie.expire", componentName, environmentName) } // ******************************************** -func DuplicateComponentOrJobNameError(duplicates []string) error { - return fmt.Errorf("duplicate component/job names %s not allowed", duplicates) +func DuplicateComponentOrJobNameErrorWithMessage(duplicates []string) error { + return errors.WithMessagef(ErrDuplicateComponentOrJobName, "duplicate component/job names %s not allowed", duplicates) } -// InvalidPortNumberError Invalid port number -func InvalidPortNumberError(value int32) error { - return fmt.Errorf("submitted configuration contains port number %d. Port numbers must be greater than or equal to %d and lower than or equal to %d", value, minimumPortNumber, maximumPortNumber) +// InvalidPortNumberErrorWithMessage Invalid port number +func InvalidPortNumberErrorWithMessage(value int32) error { + return errors.WithMessagef(ErrInvalidPortNumber, "submitted configuration contains port number %d. Port numbers must be greater than or equal to %d and lower than or equal to %d", value, minimumPortNumber, maximumPortNumber) } -func duplicateSecretName(name string) error { - return fmt.Errorf("secret has a duplicate name %s", name) +func duplicateSecretNameWithMessage(name string) error { + return errors.WithMessagef(ErrDuplicateSecretName, "secret has a duplicate name %s", name) } -func duplicateEnvVarName(name string) error { - return fmt.Errorf("environment variable has a duplicate name %s", name) +func duplicateEnvVarNameWithMessage(name string) error { + return errors.WithMessagef(DuplicateEnvVarName, "environment variable has a duplicate name %s", name) } -func duplicateAlias(alias string) error { - return fmt.Errorf("alias has a duplicate %s", alias) +func duplicateAliasWithMessage(alias string) error { + return errors.WithMessagef(ErrDuplicateAlias, "alias has a duplicate %s", alias) } -func duplicateAzureKeyVaultName(name string) error { - return fmt.Errorf("azure Key vault has a duplicate name %s", name) +func duplicateAzureKeyVaultNameWithMessage(name string) error { + return errors.WithMessagef(ErrDuplicateAzureKeyVaultName, "azure Key vault has a duplicate name %s", name) } -func secretRefEnvVarNameConflictsWithEnvironmentVariable(componentName, secretRefEnvVarName string) error { - return fmt.Errorf( - "component %s has a secret reference with environment variable name %s which exists as a regular environment variable or a secret", - componentName, secretRefEnvVarName) +func secretRefEnvVarNameConflictsWithEnvironmentVariableWithMessage(componentName, secretRefEnvVarName string) error { + return errors.WithMessagef(ErrSecretRefEnvVarNameConflictsWithEnvironmentVariable, "component %s has a secret reference with environment variable name %s which exists as a regular environment variable or a secret", componentName, secretRefEnvVarName) } -func NotValidCidrError(s string) error { - return fmt.Errorf(s) +func NotValidCidrErrorWithMessage(s string) error { + return errors.WithMessagef(ErrNotValidCidr, s) } -func NotValidIPv4CidrError(ipMask string) error { - return fmt.Errorf("%s is not a valid IPv4 mask", ipMask) +func NotValidIPv4CidrErrorWithMessage(ipMask string) error { + return errors.WithMessagef(ErrNotValidIPv4Cidr, "%s is not a valid IPv4 mask", ipMask) } -func InvalidEgressPortProtocolError(protocol string, validProtocols []string) error { - return fmt.Errorf("protocol %s must be one of {%s}", protocol, strings.Join(validProtocols, ", ")) +func InvalidEgressPortProtocolErrorWithMessage(protocol string, validProtocols []string) error { + return errors.WithMessagef(ErrInvalidEgressPortProtocol, "protocol %s must be one of {%s}", protocol, strings.Join(validProtocols, ", ")) } -func InvalidWebhookUrl(jobComponentName, environment string) error { - return getWebhookError("invalid webhook URL", jobComponentName, environment) +func InvalidWebhookUrlWithMessage(jobComponentName, environment string) error { + return getWebhookErrorWithMessage(ErrInvalidWebhookUrl, "invalid webhook URL", jobComponentName, environment) } -func NotAllowedSchemeInWebhookUrl(schema, jobComponentName, environment string) error { - return getWebhookError(fmt.Sprintf("not allowed scheme %s in the webhook in the notifications", schema), jobComponentName, environment) +func NotAllowedSchemeInWebhookUrlWithMessage(schema, jobComponentName, environment string) error { + return getWebhookErrorWithMessage(ErrNotAllowedSchemeInWebhookUrl, fmt.Sprintf("not allowed scheme %s in the webhook in the notifications", schema), jobComponentName, environment) } -func MissingPortInWebhookUrl(jobComponentName, environment string) error { - return getWebhookError("missing port in the webhook in the notifications", jobComponentName, environment) +func MissingPortInWebhookUrlWithMessage(jobComponentName, environment string) error { + return getWebhookErrorWithMessage(ErrMissingPortInWebhookUrl, "missing port in the webhook in the notifications", jobComponentName, environment) } -func OnlyAppComponentAllowedInWebhookUrl(jobComponentName, environment string) error { - return getWebhookError("webhook can only reference to an application component", jobComponentName, environment) +func OnlyAppComponentAllowedInWebhookUrlWithMessage(jobComponentName, environment string) error { + return getWebhookErrorWithMessage(ErrOnlyAppComponentAllowedInWebhookUrl, "webhook can only reference to an application component", jobComponentName, environment) } -func InvalidPortInWebhookUrl(webhookUrlPort, targetComponentName, jobComponentName, environment string) error { - return getWebhookError(fmt.Sprintf("webhook port %s does not exist in an application component %s", webhookUrlPort, targetComponentName), jobComponentName, environment) +func InvalidPortInWebhookUrlWithMessage(webhookUrlPort, targetComponentName, jobComponentName, environment string) error { + return getWebhookErrorWithMessage(ErrInvalidPortInWebhookUrl, fmt.Sprintf("webhook port %s does not exist in an application component %s", webhookUrlPort, targetComponentName), jobComponentName, environment) } -func InvalidUseOfPublicPortInWebhookUrl(webhookUrlPort, targetComponentName, jobComponentName, environment string) error { - return getWebhookError(fmt.Sprintf("not allowed to use in the webhook a public port %s of the component %s", webhookUrlPort, targetComponentName), jobComponentName, environment) +func InvalidUseOfPublicPortInWebhookUrlWithMessage(webhookUrlPort, targetComponentName, jobComponentName, environment string) error { + return getWebhookErrorWithMessage(ErrInvalidUseOfPublicPortInWebhookUrl, fmt.Sprintf("not allowed to use in the webhook a public port %s of the component %s", webhookUrlPort, targetComponentName), jobComponentName, environment) } -func getWebhookError(message, jobComponentName, environment string) error { +func getWebhookErrorWithMessage(err error, message, jobComponentName, environment string) error { componentAndEnvironmentNames := fmt.Sprintf("in the job component %s", jobComponentName) if len(environment) == 0 { - return fmt.Errorf("%s %s", message, componentAndEnvironmentNames) + return errors.WithMessagef(err, "%s %s", message, componentAndEnvironmentNames) } - return fmt.Errorf("%s %s", message, fmt.Sprintf("%s in environment %s", componentAndEnvironmentNames, environment)) + return errors.WithMessagef(err, "%s %s in environment %s", message, componentAndEnvironmentNames, environment) } -func missingIdentityError(keyVaultName, componentName string) error { - return fmt.Errorf("missing Azure identity for Azure Key Vault %s in the component %s", keyVaultName, componentName) +func MissingAzureIdentityErrorWithMessage(keyVaultName, componentName string) error { + return errors.WithMessagef(ErrMissingAzureIdentity, "missing Azure identity for Azure Key Vault %s in the component %s", keyVaultName, componentName) } diff --git a/pkg/apis/radixvalidators/validate_ra.go b/pkg/apis/radixvalidators/validate_ra.go index ef0a89451..82d8adc63 100644 --- a/pkg/apis/radixvalidators/validate_ra.go +++ b/pkg/apis/radixvalidators/validate_ra.go @@ -1,6 +1,7 @@ package radixvalidators import ( + "errors" "fmt" "net" "net/url" @@ -11,7 +12,6 @@ import ( "unicode" commonUtils "github.com/equinor/radix-common/utils" - errorUtils "github.com/equinor/radix-common/utils/errors" "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/deployment" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" @@ -37,23 +37,49 @@ var ( validOAuthSessionStoreTypes = []string{string(radixv1.SessionStoreCookie), string(radixv1.SessionStoreRedis)} validOAuthCookieSameSites = []string{string(radixv1.SameSiteStrict), string(radixv1.SameSiteLax), string(radixv1.SameSiteNone), string(radixv1.SameSiteEmpty)} blobFuse2Protocols = []string{string(radixv1.BlobFuse2ProtocolFuse2), string(radixv1.BlobFuse2ProtocolNfs)} + + requiredRadixApplicationValidators = []RadixApplicationValidator{ + validateRadixApplicationAppName, + validateComponents, + validateJobComponents, + validateNoDuplicateComponentAndJobNames, + validateEnvNames, + validateEnvironmentEgressRules, + validateVariables, + validateSecrets, + validateBranchNames, + validateDNSAppAlias, + validateDNSExternalAlias, + validatePrivateImageHubs, + validateHPAConfigForRA, + validateVolumeMountConfigForRA, + ValidateNotificationsForRA, + } ) +// RadixApplicationValidator defines a validator function for a RadixApplication +type RadixApplicationValidator func(radixApplication *radixv1.RadixApplication) error + // CanRadixApplicationBeInserted Checks if application config is valid. Returns a single error, if this is the case -func CanRadixApplicationBeInserted(client radixclient.Interface, app *radixv1.RadixApplication) (bool, error) { - isValid, errs := CanRadixApplicationBeInsertedErrors(client, app) - if isValid { - return true, nil - } +func CanRadixApplicationBeInserted(client radixclient.Interface, app *radixv1.RadixApplication, additionalValidators ...RadixApplicationValidator) error { - return false, errorUtils.Concat(errs) + validators := append(requiredRadixApplicationValidators, validateDoesRRExistFactory(client)) + validators = append(validators, additionalValidators...) + + return validateRadixApplication(app, validators...) +} + +// IsRadixApplicationValid Checks if application config is valid without server validation +func IsRadixApplicationValid(app *radixv1.RadixApplication, additionalValidators ...RadixApplicationValidator) error { + validators := append(requiredRadixApplicationValidators, additionalValidators...) + return validateRadixApplication(app, validators...) } // IsApplicationNameLowercase checks if the application name has any uppercase letters func IsApplicationNameLowercase(appName string) (bool, error) { for _, r := range appName { if unicode.IsUpper(r) && unicode.IsLetter(r) { - return false, ApplicationNameNotLowercaseError(appName) + return false, ApplicationNameNotLowercaseErrorWithMessage(appName) } } @@ -65,99 +91,35 @@ func duplicatePathForAzureKeyVault(path, azureKeyVaultName, component string) er path, azureKeyVaultName, component) } -// CanRadixApplicationBeInsertedErrors Checks if application config is valid. Returns list of errors, if present -func CanRadixApplicationBeInsertedErrors(client radixclient.Interface, app *radixv1.RadixApplication) (bool, []error) { - errs := []error{} - err := validateAppName(app.Name) - if err != nil { - errs = append(errs, err) - } - - componentErrs := validateComponents(app) - if len(componentErrs) > 0 { - errs = append(errs, componentErrs...) - } - - jobErrs := validateJobComponents(app) - if len(jobErrs) > 0 { - errs = append(errs, jobErrs...) - } - - if err = validateNoDuplicateComponentAndJobNames(app); err != nil { - errs = append(errs, err) - } - - err = validateEnvNames(app) - if err != nil { - errs = append(errs, err) - } - - errs = append(errs, validateEnvironmentEgressRules(app)...) - - err = validateVariables(app) - if err != nil { - errs = append(errs, err) - } - - err = validateSecrets(app) - if err != nil { - errs = append(errs, err) - } - - err = validateBranchNames(app) - if err != nil { - errs = append(errs, err) - } - - err = validateDoesRRExist(client, app.Name) - if err != nil { - errs = append(errs, err) - } - - dnsErrors := validateDNSAppAlias(app) - if len(dnsErrors) > 0 { - errs = append(errs, dnsErrors...) - } - - dnsErrors = validateDNSExternalAlias(app) - if len(dnsErrors) > 0 { - errs = append(errs, dnsErrors...) - } - - dnsErrors = validatePrivateImageHubs(app) - if len(dnsErrors) > 0 { - errs = append(errs, dnsErrors...) - } - - err = validateHPAConfigForRA(app) - if err != nil { - errs = append(errs, err) +func validateRadixApplication(radixApplication *radixv1.RadixApplication, validators ...RadixApplicationValidator) error { + var errs []error + for _, v := range validators { + if err := v(radixApplication); err != nil { + errs = append(errs, err) + } } - err = validateVolumeMountConfigForRA(app) - if err != nil { - errs = append(errs, err) - } + return errors.Join(errs...) +} - err = ValidateNotificationsForRA(app) - if err != nil { - errs = append(errs, err) - } +func validateRadixApplicationAppName(app *radixv1.RadixApplication) error { + return validateAppName(app.Name) +} - if len(errs) == 0 { - return true, nil +func validateDoesRRExistFactory(client radixclient.Interface) RadixApplicationValidator { + return func(radixApplication *radixv1.RadixApplication) error { + return validateDoesRRExist(client, radixApplication.Name) } - return false, errs } -func validatePrivateImageHubs(app *radixv1.RadixApplication) []error { +func validatePrivateImageHubs(app *radixv1.RadixApplication) error { var errs []error for server, config := range app.Spec.PrivateImageHubs { if config.Username == "" { - errs = append(errs, MissingPrivateImageHubUsernameError(server)) + errs = append(errs, MissingPrivateImageHubUsernameErrorWithMessage(server)) } } - return errs + return errors.Join(errs...) } // RAContainsOldPublic Checks to see if the radix config is using the deprecated config for public port @@ -170,55 +132,55 @@ func RAContainsOldPublic(app *radixv1.RadixApplication) bool { return false } -func validateDNSAppAlias(app *radixv1.RadixApplication) []error { +func validateDNSAppAlias(app *radixv1.RadixApplication) error { errs := []error{} alias := app.Spec.DNSAppAlias if alias.Component == "" && alias.Environment == "" { - return errs + return nil } if !doesEnvExist(app, alias.Environment) { - errs = append(errs, EnvForDNSAppAliasNotDefinedError(alias.Environment)) + errs = append(errs, EnvForDNSAppAliasNotDefinedErrorWithMessage(alias.Environment)) } if !doesComponentExistInEnvironment(app, alias.Component, alias.Environment) { - errs = append(errs, ComponentForDNSAppAliasNotDefinedError(alias.Component)) + errs = append(errs, ComponentForDNSAppAliasNotDefinedErrorWithMessage(alias.Component)) } - return errs + return errors.Join(errs...) } -func validateDNSExternalAlias(app *radixv1.RadixApplication) []error { +func validateDNSExternalAlias(app *radixv1.RadixApplication) error { errs := []error{} distinctAlias := make(map[string]bool) for _, externalAlias := range app.Spec.DNSExternalAlias { if externalAlias.Alias == "" && externalAlias.Component == "" && externalAlias.Environment == "" { - return errs + return nil } distinctAlias[externalAlias.Alias] = true if externalAlias.Alias == "" { - errs = append(errs, ExternalAliasCannotBeEmptyError()) + errs = append(errs, ErrExternalAliasCannotBeEmpty) } if !doesEnvExist(app, externalAlias.Environment) { - errs = append(errs, EnvForDNSExternalAliasNotDefinedError(externalAlias.Environment)) + errs = append(errs, EnvForDNSExternalAliasNotDefinedErrorWithMessage(externalAlias.Environment)) } if !doesComponentExistInEnvironment(app, externalAlias.Component, externalAlias.Environment) { - errs = append(errs, ComponentForDNSExternalAliasNotDefinedError(externalAlias.Component)) + errs = append(errs, ComponentForDNSExternalAliasNotDefinedErrorWithMessage(externalAlias.Component)) } if !doesComponentHaveAPublicPort(app, externalAlias.Component) { - errs = append(errs, ComponentForDNSExternalAliasIsNotMarkedAsPublicError(externalAlias.Component)) + errs = append(errs, ComponentForDNSExternalAliasIsNotMarkedAsPublicErrorWithMessage(externalAlias.Component)) } } if len(distinctAlias) < len(app.Spec.DNSExternalAlias) { - errs = append(errs, DuplicateExternalAliasError()) + errs = append(errs, DuplicateExternalAliasErrorWithMessage()) } - return errs + return errors.Join(errs...) } func validateNoDuplicateComponentAndJobNames(app *radixv1.RadixApplication) error { @@ -237,27 +199,27 @@ func validateNoDuplicateComponentAndJobNames(app *radixv1.RadixApplication) erro } } if len(duplicates) > 0 { - return DuplicateComponentOrJobNameError(duplicates) + return DuplicateComponentOrJobNameErrorWithMessage(duplicates) } return nil } -func validateComponents(app *radixv1.RadixApplication) []error { +func validateComponents(app *radixv1.RadixApplication) error { var errs []error for _, component := range app.Spec.Components { if component.Image != "" && (component.SourceFolder != "" || component.DockerfileName != "") { - errs = append(errs, PublicImageComponentCannotHaveSourceOrDockerfileSet(component.Name)) + errs = append(errs, PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(component.Name)) } if usesDynamicTaggingForDeployOnly(component.Image) { if len(component.EnvironmentConfig) == 0 { - errs = append(errs, ComponentWithDynamicTagRequiresTagInEnvironmentConfig(component.Name)) + errs = append(errs, ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage(component.Name)) } else { for _, environment := range component.EnvironmentConfig { if doesEnvExistAndIsMappedToBranch(app, environment.Environment) && environment.ImageTagName == "" { errs = append(errs, - ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment(component.Name, environment.Environment)) + ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironmentWithMessage(component.Name, environment.Environment)) } } } @@ -298,7 +260,7 @@ func validateComponents(app *radixv1.RadixApplication) []error { for _, environment := range component.EnvironmentConfig { if !doesEnvExist(app, environment.Environment) { - err = EnvironmentReferencedByComponentDoesNotExistError(environment.Environment, component.Name) + err = EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(environment.Environment, component.Name) errs = append(errs, err) } @@ -314,7 +276,7 @@ func validateComponents(app *radixv1.RadixApplication) []error { if environmentHasDynamicTaggingButImageLacksTag(environment.ImageTagName, component.Image) { errs = append(errs, - ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag(component.Name, environment.Environment)) + ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(component.Name, environment.Environment)) } err = validateIdentity(environment.Identity) @@ -324,24 +286,24 @@ func validateComponents(app *radixv1.RadixApplication) []error { } } - return errs + return errors.Join(errs...) } -func validateJobComponents(app *radixv1.RadixApplication) []error { +func validateJobComponents(app *radixv1.RadixApplication) error { var errs []error for _, job := range app.Spec.Jobs { if job.Image != "" && (job.SourceFolder != "" || job.DockerfileName != "") { - errs = append(errs, PublicImageComponentCannotHaveSourceOrDockerfileSet(job.Name)) + errs = append(errs, PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(job.Name)) } if usesDynamicTaggingForDeployOnly(job.Image) { if len(job.EnvironmentConfig) == 0 { - errs = append(errs, ComponentWithDynamicTagRequiresTagInEnvironmentConfig(job.Name)) + errs = append(errs, ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage(job.Name)) } else { for _, environment := range job.EnvironmentConfig { if doesEnvExistAndIsMappedToBranch(app, environment.Environment) && environment.ImageTagName == "" { errs = append(errs, - ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment(job.Name, environment.Environment)) + ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironmentWithMessage(job.Name, environment.Environment)) } } } @@ -385,7 +347,7 @@ func validateJobComponents(app *radixv1.RadixApplication) []error { for _, environment := range job.EnvironmentConfig { if !doesEnvExist(app, environment.Environment) { - err = EnvironmentReferencedByComponentDoesNotExistError(environment.Environment, job.Name) + err = EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(environment.Environment, job.Name) errs = append(errs, err) } @@ -396,7 +358,7 @@ func validateJobComponents(app *radixv1.RadixApplication) []error { if environmentHasDynamicTaggingButImageLacksTag(environment.ImageTagName, job.Image) { errs = append(errs, - ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag(job.Name, environment.Environment)) + ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(job.Name, environment.Environment)) } err = validateIdentity(environment.Identity) @@ -406,7 +368,7 @@ func validateJobComponents(app *radixv1.RadixApplication) []error { } } - return errs + return errors.Join(errs...) } func validateAuthentication(component *radixv1.RadixComponent, environments []radixv1.Environment) []error { @@ -465,7 +427,7 @@ func validateVerificationType(verificationType *radixv1.VerificationType) error actualValue := string(*verificationType) if !commonUtils.ContainsString(validValues, actualValue) { - return InvalidVerificationType(actualValue) + return InvalidVerificationTypeWithMessage(actualValue) } else { return nil } @@ -484,59 +446,59 @@ func validateOAuth(oauth *radixv1.OAuth2, componentName, environmentName string) // Validate ClientID if len(strings.TrimSpace(oauthWithDefaults.ClientID)) == 0 { - errors = append(errors, OAuthClientIdEmptyError(componentName, environmentName)) + errors = append(errors, OAuthClientIdEmptyErrorWithMessage(componentName, environmentName)) } // Validate ProxyPrefix if len(strings.TrimSpace(oauthWithDefaults.ProxyPrefix)) == 0 { - errors = append(errors, OAuthProxyPrefixEmptyError(componentName, environmentName)) + errors = append(errors, OAuthProxyPrefixEmptyErrorWithMessage(componentName, environmentName)) } else if oauthutil.SanitizePathPrefix(oauthWithDefaults.ProxyPrefix) == "/" { - errors = append(errors, OAuthProxyPrefixIsRootError(componentName, environmentName)) + errors = append(errors, OAuthProxyPrefixIsRootErrorWithMessage(componentName, environmentName)) } // Validate SessionStoreType if !commonUtils.ContainsString(validOAuthSessionStoreTypes, string(oauthWithDefaults.SessionStoreType)) { - errors = append(errors, OAuthSessionStoreTypeInvalidError(componentName, environmentName, oauthWithDefaults.SessionStoreType)) + errors = append(errors, OAuthSessionStoreTypeInvalidErrorWithMessage(componentName, environmentName, oauthWithDefaults.SessionStoreType)) } // Validate RedisStore if oauthWithDefaults.SessionStoreType == radixv1.SessionStoreRedis { if redisStore := oauthWithDefaults.RedisStore; redisStore == nil { - errors = append(errors, OAuthRedisStoreEmptyError(componentName, environmentName)) + errors = append(errors, OAuthRedisStoreEmptyErrorWithMessage(componentName, environmentName)) } else if len(strings.TrimSpace(redisStore.ConnectionURL)) == 0 { - errors = append(errors, OAuthRedisStoreConnectionURLEmptyError(componentName, environmentName)) + errors = append(errors, OAuthRedisStoreConnectionURLEmptyErrorWithMessage(componentName, environmentName)) } } // Validate OIDC config if oidc := oauthWithDefaults.OIDC; oidc == nil { - errors = append(errors, OAuthOidcEmptyError(componentName, environmentName)) + errors = append(errors, OAuthOidcEmptyErrorWithMessage(componentName, environmentName)) } else { if oidc.SkipDiscovery == nil { - errors = append(errors, OAuthOidcSkipDiscoveryEmptyError(componentName, environmentName)) + errors = append(errors, OAuthOidcSkipDiscoveryEmptyErrorWithMessage(componentName, environmentName)) } else if *oidc.SkipDiscovery { // Validate URLs when SkipDiscovery=true if len(strings.TrimSpace(oidc.JWKSURL)) == 0 { - errors = append(errors, OAuthOidcJwksUrlEmptyError(componentName, environmentName)) + errors = append(errors, OAuthOidcJwksUrlEmptyErrorWithMessage(componentName, environmentName)) } if len(strings.TrimSpace(oauthWithDefaults.LoginURL)) == 0 { - errors = append(errors, OAuthLoginUrlEmptyError(componentName, environmentName)) + errors = append(errors, OAuthLoginUrlEmptyErrorWithMessage(componentName, environmentName)) } if len(strings.TrimSpace(oauthWithDefaults.RedeemURL)) == 0 { - errors = append(errors, OAuthRedeemUrlEmptyError(componentName, environmentName)) + errors = append(errors, OAuthRedeemUrlEmptyErrorWithMessage(componentName, environmentName)) } } } // Validate Cookie if cookie := oauthWithDefaults.Cookie; cookie == nil { - errors = append(errors, OAuthCookieEmptyError(componentName, environmentName)) + errors = append(errors, OAuthCookieEmptyErrorWithMessage(componentName, environmentName)) } else { if len(strings.TrimSpace(cookie.Name)) == 0 { - errors = append(errors, OAuthCookieNameEmptyError(componentName, environmentName)) + errors = append(errors, OAuthCookieNameEmptyErrorWithMessage(componentName, environmentName)) } if !commonUtils.ContainsString(validOAuthCookieSameSites, string(cookie.SameSite)) { - errors = append(errors, OAuthCookieSameSiteInvalidError(componentName, environmentName, cookie.SameSite)) + errors = append(errors, OAuthCookieSameSiteInvalidErrorWithMessage(componentName, environmentName, cookie.SameSite)) } // Validate Expire and Refresh @@ -544,31 +506,31 @@ func validateOAuth(oauth *radixv1.OAuth2, componentName, environmentName string) expire, err := time.ParseDuration(cookie.Expire) if err != nil || expire < 0 { - errors = append(errors, OAuthCookieExpireInvalidError(componentName, environmentName, cookie.Expire)) + errors = append(errors, OAuthCookieExpireInvalidErrorWithMessage(componentName, environmentName, cookie.Expire)) expireValid = false } refresh, err := time.ParseDuration(cookie.Refresh) if err != nil || refresh < 0 { - errors = append(errors, OAuthCookieRefreshInvalidError(componentName, environmentName, cookie.Refresh)) + errors = append(errors, OAuthCookieRefreshInvalidErrorWithMessage(componentName, environmentName, cookie.Refresh)) refreshValid = false } if expireValid && refreshValid && !(refresh < expire) { - errors = append(errors, OAuthCookieRefreshMustBeLessThanExpireError(componentName, environmentName)) + errors = append(errors, OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(componentName, environmentName)) } // Validate required settings when sessionStore=cookie and cookieStore.minimal=true if oauthWithDefaults.SessionStoreType == radixv1.SessionStoreCookie && oauthWithDefaults.CookieStore != nil && oauthWithDefaults.CookieStore.Minimal != nil && *oauthWithDefaults.CookieStore.Minimal { // Refresh must be 0 if refreshValid && refresh != 0 { - errors = append(errors, OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalError(componentName, environmentName)) + errors = append(errors, OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalErrorWithMessage(componentName, environmentName)) } // SetXAuthRequestHeaders must be false if oauthWithDefaults.SetXAuthRequestHeaders == nil || *oauthWithDefaults.SetXAuthRequestHeaders { - errors = append(errors, OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersError(componentName, environmentName)) + errors = append(errors, OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersErrorWithMessage(componentName, environmentName)) } // SetAuthorizationHeader must be false if oauthWithDefaults.SetAuthorizationHeader == nil || *oauthWithDefaults.SetAuthorizationHeader { - errors = append(errors, OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderError(componentName, environmentName)) + errors = append(errors, OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderErrorWithMessage(componentName, environmentName)) } } } @@ -589,7 +551,7 @@ func environmentHasDynamicTaggingButImageLacksTag(environmentImageTag, component func validateJobSchedulerPort(job *radixv1.RadixJobComponent) error { if job.SchedulerPort == nil { - return SchedulerPortCannotBeEmptyForJobError(job.Name) + return SchedulerPortCannotBeEmptyForJobErrorWithMessage(job.Name) } return nil @@ -597,7 +559,7 @@ func validateJobSchedulerPort(job *radixv1.RadixJobComponent) error { func validateJobPayload(job *radixv1.RadixJobComponent) error { if job.Payload != nil && job.Payload.Path == "" { - return PayloadPathCannotBeEmptyForJobError(job.Name) + return PayloadPathCannotBeEmptyForJobErrorWithMessage(job.Name) } return nil @@ -607,13 +569,13 @@ func validatePorts(componentName string, ports []radixv1.ComponentPort) []error errs := []error{} if len(ports) == 0 { - err := PortSpecificationCannotBeEmptyForComponentError(componentName) + err := PortSpecificationCannotBeEmptyForComponentErrorWithMessage(componentName) errs = append(errs, err) } for _, port := range ports { if len(port.Name) > maxPortNameLength { - err := InvalidPortNameLengthError(port.Name) + err := InvalidPortNameLengthErrorWithMessage(port.Name) if err != nil { errs = append(errs, err) } @@ -625,7 +587,7 @@ func validatePorts(componentName string, ports []radixv1.ComponentPort) []error } if port.Port < minimumPortNumber || port.Port > maximumPortNumber { - if err := InvalidPortNumberError(port.Port); err != nil { + if err := InvalidPortNumberErrorWithMessage(port.Port); err != nil { errs = append(errs, err) } } @@ -646,10 +608,10 @@ func validatePublicPort(component radixv1.RadixComponent) []error { } } if matchingPortName < 1 { - errs = append(errs, PortNameIsRequiredForPublicComponentError(publicPortName, component.Name)) + errs = append(errs, PortNameIsRequiredForPublicComponentErrorWithMessage(publicPortName, component.Name)) } if matchingPortName > 1 { - errs = append(errs, MultipleMatchingPortNamesError(matchingPortName, publicPortName, component.Name)) + errs = append(errs, MultipleMatchingPortNamesErrorWithMessage(matchingPortName, publicPortName, component.Name)) } } @@ -669,7 +631,7 @@ func validateMonitoring(component radixv1.RadixCommonComponent) error { } if !isValidPort { - return MonitoringPortNameIsNotFoundComponentError(monitoringConfig.PortName, component.GetName()) + return MonitoringPortNameIsNotFoundComponentErrorWithMessage(monitoringConfig.PortName, component.GetName()) } } return nil @@ -696,7 +658,7 @@ func validateResourceRequirements(resourceRequirements *radixv1.ResourceRequirem errs = append(errs, err) } if limit, limitExist := limitQuantities[name]; limitExist && q.Cmp(limit) == 1 { - errs = append(errs, ResourceRequestOverLimitError(name, value, limit.String())) + errs = append(errs, ResourceRequestOverLimitErrorWithMessage(name, value, limit.String())) } } return errs @@ -708,17 +670,17 @@ func validateQuantity(name, value string) (resource.Quantity, error) { if name == "memory" { quantity, err = resource.ParseQuantity(value) if err != nil { - return quantity, MemoryResourceRequirementFormatError(value) + return quantity, MemoryResourceRequirementFormatErrorWithMessage(value) } } else if name == "cpu" { quantity, err = resource.ParseQuantity(value) re := regexp.MustCompile(cpuRegex) isValid := re.MatchString(value) if err != nil || !isValid { - return quantity, CPUResourceRequirementFormatError(value) + return quantity, CPUResourceRequirementFormatErrorWithMessage(value) } } else { - return quantity, InvalidResourceError(name) + return quantity, InvalidResourceErrorWithMessage(name) } return quantity, nil @@ -795,7 +757,7 @@ func validateSecretNames(resourceName string, secrets []string) error { existingSecret := make(map[string]bool) for _, secret := range secrets { if _, exists := existingSecret[secret]; exists { - return duplicateSecretName(secret) + return duplicateSecretNameWithMessage(secret) } existingSecret[secret] = true if err := validateVariableName(resourceName, secret); err != nil { @@ -826,7 +788,7 @@ func validateSecretRefs(commonComponent radixv1.RadixCommonComponent, secretRefs existingAzureKeyVaultPath := make(map[string]bool) for _, azureKeyVault := range secretRefs.AzureKeyVaults { if _, exists := existingAzureKeyVaultName[azureKeyVault.Name]; exists { - return duplicateAzureKeyVaultName(azureKeyVault.Name) + return duplicateAzureKeyVaultNameWithMessage(azureKeyVault.Name) } existingAzureKeyVaultName[azureKeyVault.Name] = true path := azureKeyVault.Path @@ -839,14 +801,14 @@ func validateSecretRefs(commonComponent radixv1.RadixCommonComponent, secretRefs useAzureIdentity := azureKeyVault.UseAzureIdentity if useAzureIdentity != nil && *useAzureIdentity { if !azureIdentityIsSet(commonComponent) { - return missingIdentityError(azureKeyVault.Name, commonComponent.GetName()) + return MissingAzureIdentityErrorWithMessage(azureKeyVault.Name, commonComponent.GetName()) } // TODO: validate for env-chain } for _, keyVaultItem := range azureKeyVault.Items { if len(keyVaultItem.EnvVar) > 0 { if _, exists := existingVariableName[keyVaultItem.EnvVar]; exists { - return duplicateEnvVarName(keyVaultItem.EnvVar) + return duplicateEnvVarNameWithMessage(keyVaultItem.EnvVar) } existingVariableName[keyVaultItem.EnvVar] = true if err := validateVariableName("Azure Key vault secret references environment variable name", keyVaultItem.EnvVar); err != nil { @@ -858,7 +820,7 @@ func validateSecretRefs(commonComponent radixv1.RadixCommonComponent, secretRefs } if keyVaultItem.Alias != nil && len(*keyVaultItem.Alias) > 0 { if _, exists := existingAlias[*keyVaultItem.Alias]; exists { - return duplicateAlias(*keyVaultItem.Alias) + return duplicateAliasWithMessage(*keyVaultItem.Alias) } existingAlias[*keyVaultItem.Alias] = true if err := validateVariableName("Azure Key vault item alias name", *keyVaultItem.Alias); err != nil { @@ -949,7 +911,7 @@ func validateVariableNames(resourceName string, variables radixv1.EnvVarsMap) er existingVariableName := make(map[string]bool) for envVarName := range variables { if _, exists := existingVariableName[envVarName]; exists { - return duplicateEnvVarName(envVarName) + return duplicateEnvVarNameWithMessage(envVarName) } existingVariableName[envVarName] = true if err := validateVariableName(resourceName, envVarName); err != nil { @@ -963,7 +925,7 @@ func validateConflictingEnvironmentAndSecretNames(componentName string, secrets for _, secret := range secrets { for _, envVarMap := range envsEnvVarMap { if _, contains := envVarMap[secret]; contains { - return SecretNameConflictsWithEnvironmentVariable(componentName, secret) + return SecretNameConflictsWithEnvironmentVariableWithMessage(componentName, secret) } } } @@ -975,7 +937,7 @@ func validateConflictingEnvironmentAndSecretRefsNames(component radixv1.RadixCom for _, item := range azureKeyVault.Items { for _, envVarMap := range envsEnvVarMap { if _, contains := envVarMap[item.EnvVar]; contains { - return secretRefEnvVarNameConflictsWithEnvironmentVariable(component.GetName(), item.EnvVar) + return secretRefEnvVarNameConflictsWithEnvironmentVariableWithMessage(component.GetName(), item.EnvVar) } } } @@ -985,7 +947,7 @@ func validateConflictingEnvironmentAndSecretRefsNames(component radixv1.RadixCom for _, item := range azureKeyVault.Items { if envVarMap, ok := envsEnvVarMap[environmentConfig.GetEnvironment()]; ok { if _, contains := envVarMap[item.EnvVar]; contains { - return secretRefEnvVarNameConflictsWithEnvironmentVariable(component.GetName(), item.EnvVar) + return secretRefEnvVarNameConflictsWithEnvironmentVariableWithMessage(component.GetName(), item.EnvVar) } } } @@ -1001,12 +963,12 @@ func validateBranchNames(app *radixv1.RadixApplication) error { } if len(env.Build.From) > 253 { - return InvalidStringValueMaxLengthError("branch from", env.Build.From, 253) + return InvalidStringValueMaxLengthErrorWithMessage("branch from", env.Build.From, 253) } isValid := branch.IsValidPattern(env.Build.From) if !isValid { - return InvalidBranchNameError(env.Build.From) + return InvalidBranchNameErrorWithMessage(env.Build.From) } } return nil @@ -1033,7 +995,7 @@ func validateMaxNameLengthForAppAndEnv(appName, envName string) error { return nil } -func validateEnvironmentEgressRules(app *radixv1.RadixApplication) []error { +func validateEnvironmentEgressRules(app *radixv1.RadixApplication) error { var errs []error for _, env := range app.Spec.Environments { if len(env.Egress.Rules) > maximumNumberOfEgressRules { @@ -1062,10 +1024,8 @@ func validateEnvironmentEgressRules(app *radixv1.RadixApplication) []error { } } } - if len(errs) != 0 { - return errs - } - return nil + + return errors.Join(errs...) } func validateEgressRulePort(port int32) error { @@ -1081,18 +1041,18 @@ func validateEgressRulePortProtocol(protocol string) error { if commonUtils.ContainsString(validProtocols, upperCaseProtocol) { return nil } else { - return InvalidEgressPortProtocolError(protocol, validProtocols) + return InvalidEgressPortProtocolErrorWithMessage(protocol, validProtocols) } } func validateEgressRuleIpMask(ipMask string) error { ipAddr, _, err := net.ParseCIDR(ipMask) if err != nil { - return NotValidCidrError(err.Error()) + return NotValidCidrErrorWithMessage(err.Error()) } ipV4Addr := ipAddr.To4() if ipV4Addr == nil { - return NotValidIPv4CidrError(ipMask) + return NotValidIPv4CidrErrorWithMessage(ipMask) } return nil @@ -1114,7 +1074,7 @@ func validateIllegalPrefixInVariableName(resourceName string, value string) erro func validateResourceWithRegexp(resourceName, value, regexpExpression string) error { if len(value) > 253 { - return InvalidStringValueMaxLengthError(resourceName, value, 253) + return InvalidStringValueMaxLengthErrorWithMessage(resourceName, value, 253) } re := regexp.MustCompile(regexpExpression) @@ -1123,7 +1083,7 @@ func validateResourceWithRegexp(resourceName, value, regexpExpression string) er if isValid { return nil } - return InvalidResourceNameError(resourceName, value) + return InvalidResourceNameErrorWithMessage(resourceName, value) } func validateHPAConfigForRA(app *radixv1.RadixApplication) error { @@ -1137,13 +1097,13 @@ func validateHPAConfigForRA(app *radixv1.RadixApplication) error { maxReplicas := envConfig.HorizontalScaling.MaxReplicas minReplicas := envConfig.HorizontalScaling.MinReplicas if maxReplicas == 0 { - return MaxReplicasForHPANotSetOrZeroError(componentName, environment) + return MaxReplicasForHPANotSetOrZeroErrorWithMessage(componentName, environment) } if minReplicas != nil && *minReplicas > maxReplicas { - return MinReplicasGreaterThanMaxReplicasError(componentName, environment) + return MinReplicasGreaterThanMaxReplicasErrorWithMessage(componentName, environment) } if envConfig.HorizontalScaling.RadixHorizontalScalingResources != nil && envConfig.HorizontalScaling.RadixHorizontalScalingResources.Cpu == nil && envConfig.HorizontalScaling.RadixHorizontalScalingResources.Memory == nil { - return NoScalingResourceSetError(componentName, environment) + return NoScalingResourceSetErrorWithMessage(componentName, environment) } } } @@ -1184,7 +1144,7 @@ func ValidateNotificationsForRA(app *radixv1.RadixApplication) error { } } } - return errorUtils.Concat(errs) + return errors.Join(errs...) } // ValidateNotifications Validate specified Notifications for the RadixApplication @@ -1195,30 +1155,30 @@ func ValidateNotifications(app *radixv1.RadixApplication, notifications *radixv1 webhook := strings.ToLower(strings.TrimSpace(*notifications.Webhook)) webhookUrl, err := url.Parse(webhook) if err != nil { - return InvalidWebhookUrl(jobComponentName, environment) + return InvalidWebhookUrlWithMessage(jobComponentName, environment) } if len(webhookUrl.Scheme) > 0 && webhookUrl.Scheme != "https" && webhookUrl.Scheme != "http" { - return NotAllowedSchemeInWebhookUrl(webhookUrl.Scheme, jobComponentName, environment) + return NotAllowedSchemeInWebhookUrlWithMessage(webhookUrl.Scheme, jobComponentName, environment) } if len(webhookUrl.Port()) == 0 { - return MissingPortInWebhookUrl(jobComponentName, environment) + return MissingPortInWebhookUrlWithMessage(jobComponentName, environment) } targetRadixComponent, targetRadixJobComponent := getRadixCommonComponentByName(app, webhookUrl.Hostname()) if targetRadixComponent == nil && targetRadixJobComponent == nil { - return OnlyAppComponentAllowedInWebhookUrl(jobComponentName, environment) + return OnlyAppComponentAllowedInWebhookUrlWithMessage(jobComponentName, environment) } if targetRadixComponent != nil { componentPort := getComponentPort(targetRadixComponent, webhookUrl.Port()) if componentPort == nil { - return InvalidPortInWebhookUrl(webhookUrl.Port(), targetRadixComponent.GetName(), jobComponentName, environment) + return InvalidPortInWebhookUrlWithMessage(webhookUrl.Port(), targetRadixComponent.GetName(), jobComponentName, environment) } if strings.EqualFold(componentPort.Name, targetRadixComponent.PublicPort) { - return InvalidUseOfPublicPortInWebhookUrl(webhookUrl.Port(), targetRadixComponent.GetName(), jobComponentName, environment) + return InvalidUseOfPublicPortInWebhookUrlWithMessage(webhookUrl.Port(), targetRadixComponent.GetName(), jobComponentName, environment) } } else if targetRadixJobComponent != nil { componentPort := getComponentPort(targetRadixJobComponent, webhookUrl.Port()) if componentPort == nil { - return InvalidPortInWebhookUrl(webhookUrl.Port(), targetRadixJobComponent.GetName(), jobComponentName, environment) + return InvalidPortInWebhookUrlWithMessage(webhookUrl.Port(), targetRadixJobComponent.GetName(), jobComponentName, environment) } } return nil @@ -1259,20 +1219,20 @@ func validateVolumeMounts(componentName, environment string, volumeMounts []radi volumeMountStorage := deployment.GetRadixVolumeMountStorage(&volumeMount) switch { case len(volumeMount.Type) == 0 && volumeMount.BlobFuse2 == nil && volumeMount.AzureFile == nil: - return emptyVolumeMountTypeOrDriverSectionError(componentName, environment) + return emptyVolumeMountTypeOrDriverSectionErrorWithMessage(componentName, environment) case multipleVolumeTypesDefined(&volumeMount): - return multipleVolumeMountTypesDefinedError(componentName, environment) + return multipleVolumeMountTypesDefinedErrorWithMessage(componentName, environment) case strings.TrimSpace(volumeMount.Name) == "" || strings.TrimSpace(volumeMount.Path) == "": - return emptyVolumeMountNameOrPathError(componentName, environment) + return emptyVolumeMountNameOrPathErrorWithMessage(componentName, environment) case volumeMount.BlobFuse2 == nil && volumeMount.AzureFile == nil && len(volumeMount.Type) > 0 && len(volumeMountStorage) == 0: - return emptyVolumeMountStorageError(componentName, environment) + return emptyVolumeMountStorageErrorWithMessage(componentName, environment) case volumeMount.BlobFuse2 != nil: switch { case len(volumeMount.BlobFuse2.Container) == 0: - return emptyBlobFuse2VolumeMountContainerError(componentName, environment) + return emptyBlobFuse2VolumeMountContainerErrorWithMessage(componentName, environment) case len(string(volumeMount.BlobFuse2.Protocol)) > 0 && !commonUtils.ContainsString(blobFuse2Protocols, string(volumeMount.BlobFuse2.Protocol)): - return unsupportedBlobFuse2VolumeMountProtocolError(componentName, environment) + return unsupportedBlobFuse2VolumeMountProtocolErrorWithMessage(componentName, environment) } fallthrough case radixv1.IsKnownVolumeMount(volumeMountType): @@ -1282,16 +1242,16 @@ func validateVolumeMounts(componentName, environment string, volumeMounts []radi } volumeMountConfigMap := mountsInComponent[volumeMountType] if _, exists := volumeMountConfigMap.names[volumeMount.Name]; exists { - return duplicateNameForVolumeMountType(volumeMount.Name, volumeMountType, componentName, environment) + return duplicateNameForVolumeMountTypeWithMessage(volumeMount.Name, volumeMountType, componentName, environment) } volumeMountConfigMap.names[volumeMount.Name] = true if _, exists := volumeMountConfigMap.path[volumeMount.Path]; exists { - return duplicatePathForVolumeMountType(volumeMount.Path, volumeMountType, componentName, environment) + return duplicatePathForVolumeMountTypeWithMessage(volumeMount.Path, volumeMountType, componentName, environment) } volumeMountConfigMap.path[volumeMount.Path] = true } default: - return unknownVolumeMountTypeError(volumeMountType, componentName, environment) + return unknownVolumeMountTypeErrorWithMessage(volumeMountType, componentName, environment) } } @@ -1331,10 +1291,10 @@ func validateAzureIdentity(azureIdentity *radixv1.AzureIdentity) error { func validateExpectedAzureIdentity(azureIdentity radixv1.AzureIdentity) error { if len(strings.TrimSpace(azureIdentity.ClientId)) == 0 { - return ResourceNameCannotBeEmptyError(azureClientIdResourceName) + return ResourceNameCannotBeEmptyErrorWithMessage(azureClientIdResourceName) } if _, err := uuid.Parse(azureIdentity.ClientId); err != nil { - return InvalidUUIDError(azureClientIdResourceName, azureIdentity.ClientId) + return InvalidUUIDErrorWithMessage(azureClientIdResourceName, azureIdentity.ClientId) } return nil } @@ -1388,7 +1348,7 @@ func validateComponentName(componentName, componentType string) error { for _, aux := range []string{defaults.OAuthProxyAuxiliaryComponentSuffix} { if strings.HasSuffix(componentName, fmt.Sprintf("-%s", aux)) { - return ComponentNameReservedSuffixError(componentName, componentType, string(aux)) + return ComponentNameReservedSuffixErrorWithMessage(componentName, componentType, string(aux)) } } return nil diff --git a/pkg/apis/radixvalidators/validate_ra_test.go b/pkg/apis/radixvalidators/validate_ra_test.go index 668fb7796..da735c179 100644 --- a/pkg/apis/radixvalidators/validate_ra_test.go +++ b/pkg/apis/radixvalidators/validate_ra_test.go @@ -5,10 +5,8 @@ import ( "strings" "testing" - "github.com/equinor/radix-common/utils/pointers" - commonUtils "github.com/equinor/radix-common/utils" - "github.com/equinor/radix-common/utils/errors" + "github.com/equinor/radix-common/utils/pointers" "github.com/equinor/radix-operator/pkg/apis/defaults" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/radixvalidators" @@ -25,19 +23,17 @@ type updateRAFunc func(rr *v1.RadixApplication) func Test_valid_ra_returns_true(t *testing.T) { _, client := validRASetup() validRA := createValidRA() - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.True(t, isValid) - assert.Nil(t, err) + assert.NoError(t, err) } func Test_missing_rr(t *testing.T) { client := radixfake.NewSimpleClientset() validRA := createValidRA() - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.False(t, isValid) - assert.NotNil(t, err) + assert.Error(t, err) } func Test_application_name_casing_is_validated(t *testing.T) { @@ -51,9 +47,9 @@ func Test_application_name_casing_is_validated(t *testing.T) { expectedError error updateRa updateRAFunc }{ - {"Mixed case name", radixvalidators.ApplicationNameNotLowercaseError(mixedCaseName), func(ra *v1.RadixApplication) { ra.Name = mixedCaseName }}, - {"Lower case name", radixvalidators.ApplicationNameNotLowercaseError(lowerCaseName), func(ra *v1.RadixApplication) { ra.Name = lowerCaseName }}, - {"Upper case name", radixvalidators.ApplicationNameNotLowercaseError(upperCaseName), func(ra *v1.RadixApplication) { ra.Name = upperCaseName }}, + {"Mixed case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(mixedCaseName), func(ra *v1.RadixApplication) { ra.Name = mixedCaseName }}, + {"Lower case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(lowerCaseName), func(ra *v1.RadixApplication) { ra.Name = lowerCaseName }}, + {"Upper case name", radixvalidators.ApplicationNameNotLowercaseErrorWithMessage(upperCaseName), func(ra *v1.RadixApplication) { ra.Name = upperCaseName }}, } for _, testcase := range testScenarios { @@ -103,87 +99,87 @@ func Test_invalid_ra(t *testing.T) { updateRA updateRAFunc }{ {"no error", nil, func(ra *v1.RadixApplication) {}}, - {"too long app name", radixvalidators.InvalidAppNameLengthError(wayTooLongName), func(ra *v1.RadixApplication) { + {"too long app name", radixvalidators.InvalidAppNameLengthErrorWithMessage(wayTooLongName), func(ra *v1.RadixApplication) { ra.Name = wayTooLongName }}, - {"invalid app name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameError("app name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid app name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage("app name", invalidResourceName), func(ra *v1.RadixApplication) { ra.Name = invalidResourceName }}, - {"empty name", radixvalidators.AppNameCannotBeEmptyError(), func(ra *v1.RadixApplication) { + {"empty name", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("app name"), func(ra *v1.RadixApplication) { ra.Name = "" }}, - {"no related rr", radixvalidators.NoRegistrationExistsForApplicationError(noReleatedRRAppName), func(ra *v1.RadixApplication) { + {"no related rr", radixvalidators.NoRegistrationExistsForApplicationErrorWithMessage(noReleatedRRAppName), func(ra *v1.RadixApplication) { ra.Name = noReleatedRRAppName }}, - {"non existing env for component", radixvalidators.EnvironmentReferencedByComponentDoesNotExistError(noExistingEnvironment, validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"non existing env for component", radixvalidators.EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(noExistingEnvironment, validRAFirstComponentName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig = []v1.RadixEnvironmentConfig{ { Environment: noExistingEnvironment, }, } }}, - {"invalid component name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameError("component name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid component name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage("component name", invalidResourceName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Name = invalidResourceName }}, - {"uppercase component name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameError("component name", invalidUpperCaseResourceName), func(ra *v1.RadixApplication) { + {"uppercase component name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage("component name", invalidUpperCaseResourceName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Name = invalidUpperCaseResourceName }}, - {"duplicate component name", radixvalidators.DuplicateComponentOrJobNameError([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { + {"duplicate component name", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { ra.Spec.Components = append(ra.Spec.Components, *ra.Spec.Components[0].DeepCopy()) }}, - {"component name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixError(oauthAuxSuffixComponentName, "component", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *v1.RadixApplication) { + {"component name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixErrorWithMessage(oauthAuxSuffixComponentName, "component", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Name = oauthAuxSuffixComponentName }}, - {"invalid port specification. Nil value", radixvalidators.PortSpecificationCannotBeEmptyForComponentError(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"invalid port specification. Nil value", radixvalidators.PortSpecificationCannotBeEmptyForComponentErrorWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Ports = nil }}, - {"invalid port specification. Empty value", radixvalidators.PortSpecificationCannotBeEmptyForComponentError(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"invalid port specification. Empty value", radixvalidators.PortSpecificationCannotBeEmptyForComponentErrorWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Ports = []v1.ComponentPort{} }}, - {"invalid port name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameError("port name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid port name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage("port name", invalidResourceName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Ports[0].Name = invalidResourceName }}, - {"too long port name", radixvalidators.InvalidPortNameLengthError(tooLongPortName), func(ra *v1.RadixApplication) { + {"too long port name", radixvalidators.InvalidPortNameLengthErrorWithMessage(tooLongPortName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].PublicPort = tooLongPortName ra.Spec.Components[0].Ports[0].Name = tooLongPortName }}, - {"invalid build secret name", radixvalidators.InvalidResourceNameError("build secret name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid build secret name", radixvalidators.InvalidResourceNameErrorWithMessage("build secret name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Build = &v1.BuildSpec{ Secrets: []string{invalidVariableName}, } }}, - {"too long build secret name", radixvalidators.InvalidStringValueMaxLengthError("build secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long build secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("build secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Build = &v1.BuildSpec{ Secrets: []string{wayTooLongName}, } }}, - {"invalid secret name", radixvalidators.InvalidResourceNameError("secret name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid secret name", radixvalidators.InvalidResourceNameErrorWithMessage("secret name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Components[1].Secrets[0] = invalidVariableName }}, - {"too long secret name", radixvalidators.InvalidStringValueMaxLengthError("secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Components[1].Secrets[0] = wayTooLongName }}, - {"invalid environment variable name", radixvalidators.InvalidResourceNameError("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Components[1].EnvironmentConfig[0].Variables[invalidVariableName] = "Any value" }}, - {"too long environment variable name", radixvalidators.InvalidStringValueMaxLengthError("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Components[1].EnvironmentConfig[0].Variables[wayTooLongName] = "Any value" }}, - {"conflicting variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariable(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { ra.Spec.Components[1].EnvironmentConfig[0].Variables[conflictingVariableName] = "Any value" ra.Spec.Components[1].Secrets[0] = conflictingVariableName }}, - {"invalid common environment variable name", radixvalidators.InvalidResourceNameError("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid common environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Components[1].Variables[invalidVariableName] = "Any value" }}, - {"too long common environment variable name", radixvalidators.InvalidStringValueMaxLengthError("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long common environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Components[1].Variables[wayTooLongName] = "Any value" }}, - {"conflicting common variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariable(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting common variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { ra.Spec.Components[1].Variables[conflictingVariableName] = "Any value" ra.Spec.Components[1].Secrets[0] = conflictingVariableName }}, - {"conflicting common variable and secret name when not environment config", radixvalidators.SecretNameConflictsWithEnvironmentVariable(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting common variable and secret name when not environment config", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage(validRASecondComponentName, conflictingVariableName), func(ra *v1.RadixApplication) { ra.Spec.Components[1].Variables[conflictingVariableName] = "Any value" ra.Spec.Components[1].Secrets[0] = conflictingVariableName ra.Spec.Components[1].EnvironmentConfig = nil @@ -191,22 +187,22 @@ func Test_invalid_ra(t *testing.T) { {"invalid number of replicas", radixvalidators.InvalidNumberOfReplicaError(radixvalidators.MaxReplica + 1), func(ra *v1.RadixApplication) { *ra.Spec.Components[0].EnvironmentConfig[0].Replicas = radixvalidators.MaxReplica + 1 }}, - {"invalid env name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameError("env name", invalidResourceName), func(ra *v1.RadixApplication) { + {"invalid env name", radixvalidators.InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage("env name", invalidResourceName), func(ra *v1.RadixApplication) { ra.Spec.Environments[0].Name = invalidResourceName }}, - {"invalid branch name", radixvalidators.InvalidBranchNameError(invalidBranchName), func(ra *v1.RadixApplication) { + {"invalid branch name", radixvalidators.InvalidBranchNameErrorWithMessage(invalidBranchName), func(ra *v1.RadixApplication) { ra.Spec.Environments[0].Build.From = invalidBranchName }}, - {"too long branch name", radixvalidators.InvalidStringValueMaxLengthError("branch from", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long branch name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("branch from", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Environments[0].Build.From = wayTooLongName }}, - {"dns alias non existing component", radixvalidators.ComponentForDNSAppAliasNotDefinedError(nonExistingComponent), func(ra *v1.RadixApplication) { + {"dns alias non existing component", radixvalidators.ComponentForDNSAppAliasNotDefinedErrorWithMessage(nonExistingComponent), func(ra *v1.RadixApplication) { ra.Spec.DNSAppAlias.Component = nonExistingComponent }}, - {"dns alias non existing env", radixvalidators.EnvForDNSAppAliasNotDefinedError(noExistingEnvironment), func(ra *v1.RadixApplication) { + {"dns alias non existing env", radixvalidators.EnvForDNSAppAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *v1.RadixApplication) { ra.Spec.DNSAppAlias.Environment = noExistingEnvironment }}, - {"dns external alias non existing component", radixvalidators.ComponentForDNSExternalAliasNotDefinedError(nonExistingComponent), func(ra *v1.RadixApplication) { + {"dns external alias non existing component", radixvalidators.ComponentForDNSExternalAliasNotDefinedErrorWithMessage(nonExistingComponent), func(ra *v1.RadixApplication) { ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ { Alias: "some.alias.com", @@ -215,7 +211,7 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"dns external alias non existing environment", radixvalidators.EnvForDNSExternalAliasNotDefinedError(noExistingEnvironment), func(ra *v1.RadixApplication) { + {"dns external alias non existing environment", radixvalidators.EnvForDNSExternalAliasNotDefinedErrorWithMessage(noExistingEnvironment), func(ra *v1.RadixApplication) { ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ { Alias: "some.alias.com", @@ -224,7 +220,7 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"dns external alias non existing alias", radixvalidators.ExternalAliasCannotBeEmptyError(), func(ra *v1.RadixApplication) { + {"dns external alias non existing alias", radixvalidators.ErrExternalAliasCannotBeEmpty, func(ra *v1.RadixApplication) { ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ { Component: ra.Spec.Components[0].Name, @@ -232,7 +228,7 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"dns external alias with no public port", radixvalidators.ComponentForDNSExternalAliasIsNotMarkedAsPublicError(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"dns external alias with no public port", radixvalidators.ComponentForDNSExternalAliasIsNotMarkedAsPublicErrorWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { // Backward compatible setting ra.Spec.Components[0].Public = false ra.Spec.Components[0].PublicPort = "" @@ -244,7 +240,7 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"duplicate dns external alias", radixvalidators.DuplicateExternalAliasError(), func(ra *v1.RadixApplication) { + {"duplicate dns external alias", radixvalidators.DuplicateExternalAliasErrorWithMessage(), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Public = true ra.Spec.DNSExternalAlias = []v1.ExternalAlias{ { @@ -259,48 +255,48 @@ func Test_invalid_ra(t *testing.T) { }, } }}, - {"resource limit unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitError("memory", "249Mi", "250Ki"), func(ra *v1.RadixApplication) { + {"memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("memory", "249Mi", "250Ki"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["memory"] = "250Ki" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["cpu"] = invalidResourceValue }}, - {"cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitError("cpu", "251m", "250m"), func(ra *v1.RadixApplication) { + {"cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("cpu", "251m", "250m"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Limits["cpu"] = "250m" ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"resource request unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"common resource limit unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"common resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"common resource request unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"common resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Resources.Requests["cpu"] = invalidResourceValue }}, {"cpu resource limit is empty", nil, func(ra *v1.RadixApplication) { @@ -317,16 +313,16 @@ func Test_invalid_ra(t *testing.T) { {"memory resource limit not set", nil, func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSet(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Image = "redis:5.0-alpine" ra.Spec.Components[0].SourceFolder = "./api" ra.Spec.Components[0].DockerfileName = ".Dockerfile" }}, - {"missing environment config for dynamic tag", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfig(validRAFirstComponentName), func(ra *v1.RadixApplication) { + {"missing environment config for dynamic tag", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage(validRAFirstComponentName), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Image = "radixcanary.azurecr.io/my-private-image:{imageTagName}" ra.Spec.Components[0].EnvironmentConfig = []v1.RadixEnvironmentConfig{} }}, - {"missing dynamic tag config for mapped environment", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment(validRAFirstComponentName, "dev"), func(ra *v1.RadixApplication) { + {"missing dynamic tag config for mapped environment", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironmentWithMessage(validRAFirstComponentName, "dev"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Image = "radixcanary.azurecr.io/my-private-image:{imageTagName}" ra.Spec.Components[0].EnvironmentConfig[0].ImageTagName = "" ra.Spec.Components[0].EnvironmentConfig = append(ra.Spec.Components[0].EnvironmentConfig, v1.RadixEnvironmentConfig{ @@ -334,111 +330,111 @@ func Test_invalid_ra(t *testing.T) { ImageTagName: "", }) }}, - {"inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag(validRAFirstComponentName, "prod"), func(ra *v1.RadixApplication) { + {"inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(validRAFirstComponentName, "prod"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Image = "radixcanary.azurecr.io/my-private-image:some-tag" ra.Spec.Components[0].EnvironmentConfig[0].ImageTagName = "any-tag" }}, - {"invalid verificationType for component", radixvalidators.InvalidVerificationType(string(invalidCertificateVerification)), func(ra *v1.RadixApplication) { + {"invalid verificationType for component", radixvalidators.InvalidVerificationTypeWithMessage(string(invalidCertificateVerification)), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Authentication = &v1.Authentication{ ClientCertificate: &v1.ClientCertificate{ Verification: &invalidCertificateVerification, }, } }}, - {"invalid verificationType for environment", radixvalidators.InvalidVerificationType(string(invalidCertificateVerification)), func(ra *v1.RadixApplication) { + {"invalid verificationType for environment", radixvalidators.InvalidVerificationTypeWithMessage(string(invalidCertificateVerification)), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Authentication = &v1.Authentication{ ClientCertificate: &v1.ClientCertificate{ Verification: &invalidCertificateVerification, }, } }}, - {"duplicate job name", radixvalidators.DuplicateComponentOrJobNameError([]string{validRAFirstJobName}), func(ra *v1.RadixApplication) { + {"duplicate job name", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstJobName}), func(ra *v1.RadixApplication) { ra.Spec.Jobs = append(ra.Spec.Jobs, *ra.Spec.Jobs[0].DeepCopy()) }}, - {"job name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixError(oauthAuxSuffixJobName, "job", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *v1.RadixApplication) { + {"job name with oauth auxiliary name suffix", radixvalidators.ComponentNameReservedSuffixErrorWithMessage(oauthAuxSuffixJobName, "job", defaults.OAuthProxyAuxiliaryComponentSuffix), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Name = oauthAuxSuffixJobName }}, - {"invalid job secret name", radixvalidators.InvalidResourceNameError("secret name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid job secret name", radixvalidators.InvalidResourceNameErrorWithMessage("secret name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Secrets[0] = invalidVariableName }}, - {"too long job secret name", radixvalidators.InvalidStringValueMaxLengthError("secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long job secret name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("secret name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Secrets[0] = wayTooLongName }}, - {"invalid job common environment variable name", radixvalidators.InvalidResourceNameError("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid job common environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Variables[invalidVariableName] = "Any value" }}, - {"too long job common environment variable name", radixvalidators.InvalidStringValueMaxLengthError("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long job common environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Variables[wayTooLongName] = "Any value" }}, - {"invalid job environment variable name", radixvalidators.InvalidResourceNameError("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { + {"invalid job environment variable name", radixvalidators.InvalidResourceNameErrorWithMessage("environment variable name", invalidVariableName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Variables[invalidVariableName] = "Any value" }}, - {"too long job environment variable name", radixvalidators.InvalidStringValueMaxLengthError("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { + {"too long job environment variable name", radixvalidators.InvalidStringValueMaxLengthErrorWithMessage("environment variable name", wayTooLongName, 253), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Variables[wayTooLongName] = "Any value" }}, - {"conflicting job variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariable("job", conflictingVariableName), func(ra *v1.RadixApplication) { + {"conflicting job variable and secret name", radixvalidators.SecretNameConflictsWithEnvironmentVariableWithMessage("job", conflictingVariableName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Variables[conflictingVariableName] = "Any value" ra.Spec.Jobs[0].Secrets[0] = conflictingVariableName }}, - {"non existing env for job", radixvalidators.EnvironmentReferencedByComponentDoesNotExistError(noExistingEnvironment, validRAFirstJobName), func(ra *v1.RadixApplication) { + {"non existing env for job", radixvalidators.EnvironmentReferencedByComponentDoesNotExistErrorWithMessage(noExistingEnvironment, validRAFirstJobName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig = []v1.RadixJobComponentEnvironmentConfig{ { Environment: noExistingEnvironment, }, } }}, - {"scheduler port is not set", radixvalidators.SchedulerPortCannotBeEmptyForJobError(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"scheduler port is not set", radixvalidators.SchedulerPortCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].SchedulerPort = nil }}, - {"payload is empty struct", radixvalidators.PayloadPathCannotBeEmptyForJobError(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"payload is empty struct", radixvalidators.PayloadPathCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Payload = &v1.RadixJobComponentPayload{} }}, - {"payload path is empty string", radixvalidators.PayloadPathCannotBeEmptyForJobError(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"payload path is empty string", radixvalidators.PayloadPathCannotBeEmptyForJobErrorWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Payload = &v1.RadixJobComponentPayload{Path: ""} }}, - {"job resource limit unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"job resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"job memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"job memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"job memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitError("memory", "249Mi", "250Ki"), func(ra *v1.RadixApplication) { + {"job memory resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("memory", "249Mi", "250Ki"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["memory"] = "250Ki" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"job cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"job cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["cpu"] = invalidResourceValue }}, - {"job cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitError("cpu", "251m", "250m"), func(ra *v1.RadixApplication) { + {"job cpu resource request larger than limit", radixvalidators.ResourceRequestOverLimitErrorWithMessage("cpu", "251m", "250m"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Limits["cpu"] = "250m" ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["cpu"] = "251m" }}, - {"job resource request unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"job resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"job common resource limit unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"job common resource limit unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits[unsupportedResource] = "250m" }}, - {"job common resource request unsupported resource", radixvalidators.InvalidResourceError(unsupportedResource), func(ra *v1.RadixApplication) { + {"job common resource request unsupported resource", radixvalidators.InvalidResourceErrorWithMessage(unsupportedResource), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Resources.Requests[unsupportedResource] = "250m" }}, - {"job common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common memory resource limit wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["memory"] = invalidResourceValue }}, - {"job common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common memory resource request wrong format", radixvalidators.MemoryResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Resources.Requests["memory"] = invalidResourceValue }}, - {"job common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common cpu resource limit wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Resources.Limits["cpu"] = invalidResourceValue }}, - {"job common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatError(invalidResourceValue), func(ra *v1.RadixApplication) { + {"job common cpu resource request wrong format", radixvalidators.CPUResourceRequirementFormatErrorWithMessage(invalidResourceValue), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Resources.Requests["cpu"] = invalidResourceValue }}, {"job cpu resource limit is empty", nil, func(ra *v1.RadixApplication) { @@ -455,16 +451,16 @@ func Test_invalid_ra(t *testing.T) { {"job memory resource limit not set", nil, func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Resources.Requests["memory"] = "249Mi" }}, - {"job wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSet(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"job wrong public image config", radixvalidators.PublicImageComponentCannotHaveSourceOrDockerfileSetWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Image = "redis:5.0-alpine" ra.Spec.Jobs[0].SourceFolder = "./api" ra.Spec.Jobs[0].DockerfileName = ".Dockerfile" }}, - {"job missing environment config for dynamic tag", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfig(validRAFirstJobName), func(ra *v1.RadixApplication) { + {"job missing environment config for dynamic tag", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfigWithMessage(validRAFirstJobName), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Image = "radixcanary.azurecr.io/my-private-image:{imageTagName}" ra.Spec.Jobs[0].EnvironmentConfig = []v1.RadixJobComponentEnvironmentConfig{} }}, - {"job missing dynamic tag config for mapped environment", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironment(validRAFirstJobName, "dev"), func(ra *v1.RadixApplication) { + {"job missing dynamic tag config for mapped environment", radixvalidators.ComponentWithDynamicTagRequiresTagInEnvironmentConfigForEnvironmentWithMessage(validRAFirstJobName, "dev"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Image = "radixcanary.azurecr.io/my-private-image:{imageTagName}" ra.Spec.Jobs[0].EnvironmentConfig[0].ImageTagName = "" ra.Spec.Jobs[0].EnvironmentConfig = append(ra.Spec.Jobs[0].EnvironmentConfig, v1.RadixJobComponentEnvironmentConfig{ @@ -472,7 +468,7 @@ func Test_invalid_ra(t *testing.T) { ImageTagName: "", }) }}, - {"job inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTag(validRAFirstJobName, "dev"), func(ra *v1.RadixApplication) { + {"job inconcistent dynamic tag config for environment", radixvalidators.ComponentWithTagInEnvironmentConfigForEnvironmentRequiresDynamicTagWithMessage(validRAFirstJobName, "dev"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Image = "radixcanary.azurecr.io/my-private-image:some-tag" ra.Spec.Jobs[0].EnvironmentConfig[0].ImageTagName = "any-tag" }}, @@ -480,22 +476,22 @@ func Test_invalid_ra(t *testing.T) { ra.Name = name50charsLong ra.Spec.Environments = append(ra.Spec.Environments, v1.Environment{Name: "extra-14-chars"}) }}, - {"missing OAuth clientId for dev env - common OAuth config", radixvalidators.OAuthClientIdEmptyError(validRAFirstComponentName, "dev"), func(rr *v1.RadixApplication) { + {"missing OAuth clientId for dev env - common OAuth config", radixvalidators.OAuthClientIdEmptyErrorWithMessage(validRAFirstComponentName, "dev"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].Authentication.OAuth2 = &v1.OAuth2{} }}, - {"missing OAuth clientId for prod env - environmentConfig OAuth config", radixvalidators.OAuthClientIdEmptyError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"missing OAuth clientId for prod env - environmentConfig OAuth config", radixvalidators.OAuthClientIdEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.ClientID = "" }}, - {"OAuth path prefix is root", radixvalidators.OAuthProxyPrefixIsRootError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"OAuth path prefix is root", radixvalidators.OAuthProxyPrefixIsRootErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.ProxyPrefix = "/" }}, - {"invalid OAuth session store type", radixvalidators.OAuthSessionStoreTypeInvalidError(validRAFirstComponentName, "prod", "invalid-store"), func(rr *v1.RadixApplication) { + {"invalid OAuth session store type", radixvalidators.OAuthSessionStoreTypeInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-store"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = "invalid-store" }}, - {"missing OAuth redisStore property", radixvalidators.OAuthRedisStoreEmptyError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"missing OAuth redisStore property", radixvalidators.OAuthRedisStoreEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedisStore = nil }}, - {"missing OAuth redis connection URL", radixvalidators.OAuthRedisStoreConnectionURLEmptyError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"missing OAuth redis connection URL", radixvalidators.OAuthRedisStoreConnectionURLEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedisStore.ConnectionURL = "" }}, {"no error when skipDiscovery=true and login, redeem and jwks urls set", nil, func(rr *v1.RadixApplication) { @@ -506,21 +502,21 @@ func Test_invalid_ra(t *testing.T) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.LoginURL = "loginurl" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedeemURL = "redeemurl" }}, - {"error when skipDiscovery=true and missing loginUrl", radixvalidators.OAuthLoginUrlEmptyError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"error when skipDiscovery=true and missing loginUrl", radixvalidators.OAuthLoginUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), JWKSURL: "jwksurl", } rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.RedeemURL = "redeemurl" }}, - {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthRedeemUrlEmptyError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthRedeemUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), JWKSURL: "jwksurl", } rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.LoginURL = "loginurl" }}, - {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthOidcJwksUrlEmptyError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"error when skipDiscovery=true and missing redeemUrl", radixvalidators.OAuthOidcJwksUrlEmptyErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.OIDC = &v1.OAuth2OIDC{ SkipDiscovery: commonUtils.BoolPtr(true), } @@ -537,7 +533,7 @@ func Test_invalid_ra(t *testing.T) { Refresh: "0s", } }}, - {"error when cookieStore.minimal=true and SetAuthorizationHeader=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"error when cookieStore.minimal=true and SetAuthorizationHeader=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetAuthorizationHeaderErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(true) @@ -547,7 +543,7 @@ func Test_invalid_ra(t *testing.T) { Refresh: "0s", } }}, - {"error when cookieStore.minimal=true and SetXAuthRequestHeaders=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"error when cookieStore.minimal=true and SetXAuthRequestHeaders=true", radixvalidators.OAuthCookieStoreMinimalIncorrectSetXAuthRequestHeadersErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(false) @@ -557,7 +553,7 @@ func Test_invalid_ra(t *testing.T) { Refresh: "0s", } }}, - {"error when cookieStore.minimal=true and Cookie.Refresh>0", radixvalidators.OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"error when cookieStore.minimal=true and Cookie.Refresh>0", radixvalidators.OAuthCookieStoreMinimalIncorrectCookieRefreshIntervalErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SessionStoreType = v1.SessionStoreCookie rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.CookieStore = &v1.OAuth2CookieStore{Minimal: commonUtils.BoolPtr(true)} rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.SetAuthorizationHeader = commonUtils.BoolPtr(false) @@ -567,62 +563,62 @@ func Test_invalid_ra(t *testing.T) { Refresh: "1s", } }}, - {"invalid OAuth cookie same site", radixvalidators.OAuthCookieSameSiteInvalidError(validRAFirstComponentName, "prod", "invalid-samesite"), func(rr *v1.RadixApplication) { + {"invalid OAuth cookie same site", radixvalidators.OAuthCookieSameSiteInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-samesite"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.SameSite = "invalid-samesite" }}, - {"invalid OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidError(validRAFirstComponentName, "prod", "invalid-expire"), func(rr *v1.RadixApplication) { + {"invalid OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-expire"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "invalid-expire" }}, - {"negative OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidError(validRAFirstComponentName, "prod", "-1s"), func(rr *v1.RadixApplication) { + {"negative OAuth cookie expire timeframe", radixvalidators.OAuthCookieExpireInvalidErrorWithMessage(validRAFirstComponentName, "prod", "-1s"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "-1s" }}, - {"invalid OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidError(validRAFirstComponentName, "prod", "invalid-refresh"), func(rr *v1.RadixApplication) { + {"invalid OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidErrorWithMessage(validRAFirstComponentName, "prod", "invalid-refresh"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "invalid-refresh" }}, - {"negative OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidError(validRAFirstComponentName, "prod", "-1s"), func(rr *v1.RadixApplication) { + {"negative OAuth cookie refresh time frame", radixvalidators.OAuthCookieRefreshInvalidErrorWithMessage(validRAFirstComponentName, "prod", "-1s"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "-1s" }}, - {"oauth cookie expire equals refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"oauth cookie expire equals refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "1h" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "1h" }}, - {"oauth cookie expire less than refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireError(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { + {"oauth cookie expire less than refresh", radixvalidators.OAuthCookieRefreshMustBeLessThanExpireErrorWithMessage(validRAFirstComponentName, "prod"), func(rr *v1.RadixApplication) { rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Expire = "30m" rr.Spec.Components[0].EnvironmentConfig[0].Authentication.OAuth2.Cookie.Refresh = "1h" }}, - {"duplicate name in job/component boundary", radixvalidators.DuplicateComponentOrJobNameError([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { + {"duplicate name in job/component boundary", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { job := *ra.Spec.Jobs[0].DeepCopy() job.Name = validRAFirstComponentName ra.Spec.Jobs = append(ra.Spec.Jobs, job) }}, - {"no mask size postfix in egress rule destination", radixvalidators.DuplicateComponentOrJobNameError([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { + {"no mask size postfix in egress rule destination", radixvalidators.DuplicateComponentOrJobNameErrorWithMessage([]string{validRAFirstComponentName}), func(ra *v1.RadixApplication) { job := *ra.Spec.Jobs[0].DeepCopy() job.Name = validRAFirstComponentName ra.Spec.Jobs = append(ra.Spec.Jobs, job) }}, - {"identity.azure.clientId cannot be empty for component", radixvalidators.ResourceNameCannotBeEmptyError("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for component", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Identity.Azure.ClientId = " " }}, - {"identity.azure.clientId cannot be empty for component environment config", radixvalidators.ResourceNameCannotBeEmptyError("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for component environment config", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Identity.Azure.ClientId = " " }}, - {"identity.azure.clientId cannot be empty for job", radixvalidators.ResourceNameCannotBeEmptyError("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for job", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Identity.Azure.ClientId = " " }}, - {"identity.azure.clientId cannot be empty for job environment config", radixvalidators.ResourceNameCannotBeEmptyError("identity.azure.clientId"), func(ra *v1.RadixApplication) { + {"identity.azure.clientId cannot be empty for job environment config", radixvalidators.ResourceNameCannotBeEmptyErrorWithMessage("identity.azure.clientId"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Identity.Azure.ClientId = " " }}, - {"invalid identity.azure.clientId for component", radixvalidators.InvalidUUIDError("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for component", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].Identity.Azure.ClientId = "1111-22-33-44" }}, - {"invalid identity.azure.clientId for component environment config", radixvalidators.InvalidUUIDError("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for component environment config", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { ra.Spec.Components[0].EnvironmentConfig[0].Identity.Azure.ClientId = "1111-22-33-44" }}, - {"invalid identity.azure.clientId for job", radixvalidators.InvalidUUIDError("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for job", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Identity.Azure.ClientId = "1111-22-33-44" }}, - {"invalid identity.azure.clientId for job environment config", radixvalidators.InvalidUUIDError("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { + {"invalid identity.azure.clientId for job environment config", radixvalidators.InvalidUUIDErrorWithMessage("identity.azure.clientId", "1111-22-33-44"), func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Identity.Azure.ClientId = "1111-22-33-44" }}, } @@ -632,21 +628,27 @@ func Test_invalid_ra(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, errs := radixvalidators.CanRadixApplicationBeInsertedErrors(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) if testcase.expectedError != nil { - assert.False(t, isValid) - assert.NotNil(t, errs) - - assert.Truef(t, errors.Contains(errs, testcase.expectedError), "Expected error is not contained in list of errors") + assert.Error(t, err) + assertErrorCauseIs(t, err, testcase.expectedError, "Expected error is not contained in list of errors") } else { - assert.True(t, isValid) - assert.Nil(t, errs) + assert.NoError(t, err) } }) } } +func assertErrorCauseIs(t *testing.T, err error, expectedError error, msgAndArgs ...interface{}) { + assert.Contains(t, err.Error(), expectedError.Error(), msgAndArgs...) + + // If expecedError exposes a Cause method, lets check if the return error has the same cause error + if x, ok := expectedError.(interface{ Cause() error }); ok { + assert.ErrorIs(t, err, x.Cause(), msgAndArgs...) + } +} + func Test_ValidRAComponentLimitRequest_NoError(t *testing.T) { var testScenarios = []struct { name string @@ -723,10 +725,9 @@ func Test_ValidRAComponentLimitRequest_NoError(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.True(t, isValid) - assert.Nil(t, err) + assert.NoError(t, err) }) } } @@ -807,10 +808,9 @@ func Test_ValidRAJobLimitRequest_NoError(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.True(t, isValid) - assert.Nil(t, err) + assert.NoError(t, err) }) } } @@ -843,10 +843,9 @@ func Test_InvalidRAComponentLimitRequest_Error(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.False(t, isValid) - assert.NotNil(t, err) + assert.Error(t, err) }) } } @@ -879,10 +878,9 @@ func Test_InvalidRAJobLimitRequest_Error(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.False(t, isValid) - assert.NotNil(t, err) + assert.Error(t, err) }) } } @@ -1021,14 +1019,15 @@ func Test_PublicPort(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - isErrorNil := false - if err == nil { - isErrorNil = true + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + + if testcase.isValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) } - assert.Equal(t, testcase.isValid, isValid) - assert.Equal(t, testcase.isErrorNil, isErrorNil) + assert.Equal(t, testcase.isErrorNil, err == nil) }) } } @@ -1103,14 +1102,15 @@ func Test_Variables(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - isErrorNil := false - if err == nil { - isErrorNil = true + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + + if testcase.isValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) } - assert.Equal(t, testcase.isValid, isValid) - assert.Equal(t, testcase.isErrorNil, isErrorNil) + assert.Equal(t, testcase.isErrorNil, err == nil) }) } } @@ -1416,13 +1416,10 @@ func Test_ValidationOfVolumeMounts_Errors(t *testing.T) { validRA := createValidRA() volumes := testcase.volumeMounts() ra(validRA, volumes) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - isErrorNil := false - if err == nil { - isErrorNil = true - } + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + isErrorNil := err == nil - assert.Equal(t, testcase.isValid, isValid) + assert.Equal(t, testcase.isValid, err == nil) assert.Equal(t, testcase.isErrorNil, isErrorNil) if !isErrorNil { assert.Contains(t, err.Error(), testcase.testContainedByError) @@ -1514,14 +1511,15 @@ func Test_ValidHPA_NoError(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - isErrorNil := false - if err == nil { - isErrorNil = true + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + + if testcase.isValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) } - assert.Equal(t, testcase.isValid, isValid) - assert.Equal(t, testcase.isErrorNil, isErrorNil) + assert.Equal(t, testcase.isErrorNil, err == nil) }) } } @@ -1689,8 +1687,14 @@ func Test_EgressConfig(t *testing.T) { t.Run(testcase.name, func(t *testing.T) { validRA := createValidRA() testcase.updateRA(validRA) - isValid, _ := radixvalidators.CanRadixApplicationBeInserted(client, validRA) - assert.Equal(t, testcase.isValid, isValid) + err := radixvalidators.CanRadixApplicationBeInserted(client, validRA) + + if testcase.isValid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) } } @@ -1732,72 +1736,72 @@ func Test_validateNotificationsRA(t *testing.T) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8099/abc")} }, }, - {name: "Invalid webhook URL", expectedError: radixvalidators.InvalidWebhookUrl("job", ""), + {name: "Invalid webhook URL", expectedError: radixvalidators.InvalidWebhookUrlWithMessage("job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: &invalidUrl} }, }, - {name: "Invalid webhook URL in environment", expectedError: radixvalidators.InvalidWebhookUrl("job", "dev"), + {name: "Invalid webhook URL in environment", expectedError: radixvalidators.InvalidWebhookUrlWithMessage("job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: &invalidUrl} }, }, - {name: "Not allowed scheme ftp", expectedError: radixvalidators.NotAllowedSchemeInWebhookUrl("ftp", "job", ""), + {name: "Not allowed scheme ftp", expectedError: radixvalidators.NotAllowedSchemeInWebhookUrlWithMessage("ftp", "job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("ftp://api:8090")} }, }, - {name: "Not allowed scheme ftp in environment", expectedError: radixvalidators.NotAllowedSchemeInWebhookUrl("ftp", "job", "dev"), + {name: "Not allowed scheme ftp in environment", expectedError: radixvalidators.NotAllowedSchemeInWebhookUrlWithMessage("ftp", "job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("ftp://api:8090")} }, }, - {name: "missing port in the webhook", expectedError: radixvalidators.MissingPortInWebhookUrl("job", ""), + {name: "missing port in the webhook", expectedError: radixvalidators.MissingPortInWebhookUrlWithMessage("job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api/abc")} }, }, - {name: "missing port in the webhook in environment", expectedError: radixvalidators.MissingPortInWebhookUrl("job", "dev"), + {name: "missing port in the webhook in environment", expectedError: radixvalidators.MissingPortInWebhookUrlWithMessage("job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api/abc")} }, }, - {name: "webhook can only reference to an application component or job", expectedError: radixvalidators.OnlyAppComponentAllowedInWebhookUrl("job", ""), + {name: "webhook can only reference to an application component or job", expectedError: radixvalidators.OnlyAppComponentAllowedInWebhookUrlWithMessage("job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://notexistingcomponent:8090/abc")} }, }, - {name: "webhook can only reference to an application component or job in environment", expectedError: radixvalidators.OnlyAppComponentAllowedInWebhookUrl("job", "dev"), + {name: "webhook can only reference to an application component or job in environment", expectedError: radixvalidators.OnlyAppComponentAllowedInWebhookUrlWithMessage("job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://notexistingcomponent:8090/abc")} }, }, - {name: "webhook port does not exist in an application component", expectedError: radixvalidators.InvalidPortInWebhookUrl("8077", "api", "job", ""), + {name: "webhook port does not exist in an application component", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "api", "job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api:8077")} }, }, - {name: "webhook port does not exist in an application component in environment", expectedError: radixvalidators.InvalidPortInWebhookUrl("8077", "api", "job", "dev"), + {name: "webhook port does not exist in an application component in environment", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "api", "job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://api:8077")} }, }, - {name: "webhook port does not exist in an application job component", expectedError: radixvalidators.InvalidPortInWebhookUrl("8077", "job3", "job", ""), + {name: "webhook port does not exist in an application job component", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "job3", "job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8077")} }, }, - {name: "webhook port does not exist in an application job component in environment", expectedError: radixvalidators.InvalidPortInWebhookUrl("8077", "job3", "job", "dev"), + {name: "webhook port does not exist in an application job component in environment", expectedError: radixvalidators.InvalidPortInWebhookUrlWithMessage("8077", "job3", "job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://job3:8077")} }, }, - {name: "not allowed to use in the webhook a public port of an application component", expectedError: radixvalidators.InvalidUseOfPublicPortInWebhookUrl("8080", "app", "job", ""), + {name: "not allowed to use in the webhook a public port of an application component", expectedError: radixvalidators.InvalidUseOfPublicPortInWebhookUrlWithMessage("8080", "app", "job", ""), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://app:8080")} }, }, - {name: "not allowed to use in the webhook a public port of an application component in environment", expectedError: radixvalidators.InvalidUseOfPublicPortInWebhookUrl("8080", "app", "job", "dev"), + {name: "not allowed to use in the webhook a public port of an application component in environment", expectedError: radixvalidators.InvalidUseOfPublicPortInWebhookUrlWithMessage("8080", "app", "job", "dev"), updateRa: func(ra *v1.RadixApplication) { ra.Spec.Jobs[0].EnvironmentConfig[0].Notifications = &v1.Notifications{Webhook: pointers.Ptr("http://app:8080")} }, diff --git a/pkg/apis/radixvalidators/validate_rd.go b/pkg/apis/radixvalidators/validate_rd.go index 44c1a9d71..5719be21e 100644 --- a/pkg/apis/radixvalidators/validate_rd.go +++ b/pkg/apis/radixvalidators/validate_rd.go @@ -6,7 +6,6 @@ import ( "github.com/equinor/radix-common/utils/errors" radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" - radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" ) const ( @@ -42,7 +41,7 @@ func GitTagsContainIllegalChars(gitTags string) error { } // CanRadixDeploymentBeInserted Checks if RD is valid -func CanRadixDeploymentBeInserted(client radixclient.Interface, deploy *radixv1.RadixDeployment) (bool, error) { +func CanRadixDeploymentBeInserted(deploy *radixv1.RadixDeployment) error { // todo! ensure that all rules are valid errs := []error{} err := validateAppName(deploy.Name) @@ -63,10 +62,7 @@ func CanRadixDeploymentBeInserted(client radixclient.Interface, deploy *radixv1. errs = append(errs, err) } - if len(errs) <= 0 { - return true, nil - } - return false, errors.Concat(errs) + return errors.Concat(errs) } func validateDeployComponents(deployment *radixv1.RadixDeployment) []error { @@ -124,10 +120,10 @@ func validateHPAConfigForRD(component *radixv1.RadixDeployComponent, environment minReplicas := component.HorizontalScaling.MinReplicas if maxReplicas == 0 { - return MaxReplicasForHPANotSetOrZeroError(component.Name, environmentName) + return MaxReplicasForHPANotSetOrZeroErrorWithMessage(component.Name, environmentName) } if minReplicas != nil && *minReplicas > maxReplicas { - return MinReplicasGreaterThanMaxReplicasError(component.Name, environmentName) + return MinReplicasGreaterThanMaxReplicasErrorWithMessage(component.Name, environmentName) } } @@ -136,7 +132,7 @@ func validateHPAConfigForRD(component *radixv1.RadixDeployComponent, environment func validateDeployJobSchedulerPort(job *radixv1.RadixDeployJobComponent) error { if job.SchedulerPort == nil { - return SchedulerPortCannotBeEmptyForJobError(job.Name) + return SchedulerPortCannotBeEmptyForJobErrorWithMessage(job.Name) } return nil @@ -144,7 +140,7 @@ func validateDeployJobSchedulerPort(job *radixv1.RadixDeployJobComponent) error func validateDeployJobPayload(job *radixv1.RadixDeployJobComponent) error { if job.Payload != nil && job.Payload.Path == "" { - return PayloadPathCannotBeEmptyForJobError(job.Name) + return PayloadPathCannotBeEmptyForJobErrorWithMessage(job.Name) } return nil diff --git a/pkg/apis/radixvalidators/validate_rd_test.go b/pkg/apis/radixvalidators/validate_rd_test.go index eb29f0628..b9b38300a 100644 --- a/pkg/apis/radixvalidators/validate_rd_test.go +++ b/pkg/apis/radixvalidators/validate_rd_test.go @@ -6,20 +6,14 @@ import ( v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/radixvalidators" "github.com/equinor/radix-operator/pkg/apis/utils" - radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" - radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" "github.com/stretchr/testify/assert" - "k8s.io/client-go/kubernetes" - kubefake "k8s.io/client-go/kubernetes/fake" ) func Test_valid_rd_returns_true(t *testing.T) { validRD := createValidRD() - _, radixclient := validRDSetup() - isValid, err := radixvalidators.CanRadixDeploymentBeInserted(radixclient, validRD) + err := radixvalidators.CanRadixDeploymentBeInserted(validRD) - assert.True(t, isValid) - assert.Nil(t, err) + assert.NoError(t, err) } type updateRDFunc func(rr *v1.RadixDeployment) @@ -69,16 +63,13 @@ func Test_invalid_rd_returns_false(t *testing.T) { }}, } - _, client := validRRSetup() - for _, testcase := range testScenarios { t.Run(testcase.name, func(t *testing.T) { validRD := createValidRD() testcase.updateRD(validRD) - isValid, err := radixvalidators.CanRadixDeploymentBeInserted(client, validRD) + err := radixvalidators.CanRadixDeploymentBeInserted(validRD) - assert.False(t, isValid) - assert.NotNil(t, err) + assert.Error(t, err) }) } } @@ -102,11 +93,3 @@ func createValidRD() *v1.RadixDeployment { return validRD } - -func validRDSetup() (kubernetes.Interface, radixclient.Interface) { - validRR, _ := utils.GetRadixRegistrationFromFile("testdata/radixregistration.yaml") - kubeclient := kubefake.NewSimpleClientset() - client := radixfake.NewSimpleClientset(validRR) - - return kubeclient, client -} diff --git a/pkg/apis/radixvalidators/validate_rr.go b/pkg/apis/radixvalidators/validate_rr.go index e20be8aad..dd3089d3f 100644 --- a/pkg/apis/radixvalidators/validate_rr.go +++ b/pkg/apis/radixvalidators/validate_rr.go @@ -2,12 +2,12 @@ package radixvalidators import ( "context" - "errors" + errors2 "errors" "fmt" "regexp" "strings" - errorUtils "github.com/equinor/radix-common/utils/errors" + "github.com/equinor/radix-common/utils/errors" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/utils/branch" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" @@ -16,11 +16,12 @@ import ( ) const ( - radixConfigFullNamePattern = "^(\\/*[a-zA-Z0-9_\\.\\-]+)+((\\.yaml)|(\\.yml))$" - invalidRadixConfigFullNameErrorMessage = "invalid file name for radixconfig. See https://www.radix.equinor.com/references/reference-radix-config/ for more information" + radixConfigFullNamePattern = `^(\/*[a-zA-Z0-9_\.\-]+)+((\.yaml)|(\.yml))$` ) var ( + ErrInvalidRadixConfigFullName = errors2.New("invalid file name for radixconfig. See https://www.radix.equinor.com/references/reference-radix-config/ for more information") + requiredRadixRegistrationValidators []RadixRegistrationValidator = []RadixRegistrationValidator{ validateRadixRegistrationAppName, validateRadixRegistrationGitSSHUrl, @@ -37,7 +38,7 @@ type RadixRegistrationValidator func(radixRegistration *v1.RadixRegistration) er // RequireConfigurationItem validates that ConfigurationItem for a RadixRegistration set func RequireConfigurationItem(rr *v1.RadixRegistration) error { if len(strings.TrimSpace(rr.Spec.ConfigurationItem)) == 0 { - return ResourceNameCannotBeEmptyError("configuration item") + return ResourceNameCannotBeEmptyErrorWithMessage("configuration item") } return nil @@ -46,7 +47,7 @@ func RequireConfigurationItem(rr *v1.RadixRegistration) error { // RequireAdGroups validates that AdGroups contains minimum one item func RequireAdGroups(rr *v1.RadixRegistration) error { if len(rr.Spec.AdGroups) == 0 { - return ResourceNameCannotBeEmptyError("AD groups") + return ResourceNameCannotBeEmptyErrorWithMessage("AD groups") } return nil @@ -73,7 +74,7 @@ func validateRadixRegistration(radixRegistration *v1.RadixRegistration, validato errs = append(errs, err) } } - return errorUtils.Concat(errs) + return errors.Concat(errs) } // GetRadixRegistrationBeInsertedWarnings Get warnings for inserting RadixRegistration @@ -114,25 +115,25 @@ func validateRadixRegistrationConfigurationItem(rr *v1.RadixRegistration) error func validateConfigurationItem(value string) error { if len(value) > 100 { - return InvalidStringValueMaxLengthError("configuration item", value, 100) + return InvalidStringValueMaxLengthErrorWithMessage("configuration item", value, 100) } return nil } func validateRequiredResourceName(resourceName, value string) error { if len(value) > 253 { - return InvalidStringValueMaxLengthError(resourceName, value, 253) + return InvalidStringValueMaxLengthErrorWithMessage(resourceName, value, 253) } if value == "" { - return ResourceNameCannotBeEmptyError(resourceName) + return ResourceNameCannotBeEmptyErrorWithMessage(resourceName) } re := regexp.MustCompile(`^(([a-z0-9][-a-z0-9.]*)?[a-z0-9])?$`) isValid := re.MatchString(value) if !isValid { - return InvalidLowerCaseAlphaNumericDotDashResourceNameError(resourceName, value) + return InvalidLowerCaseAlphaNumericDotDashResourceNameErrorWithMessage(resourceName, value) } return nil @@ -203,7 +204,7 @@ func validateDoesRRExist(client radixclient.Interface, appName string) error { _, err := client.RadixV1().RadixRegistrations().Get(context.TODO(), appName, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { - return NoRegistrationExistsForApplicationError(appName) + return NoRegistrationExistsForApplicationErrorWithMessage(appName) } return err } @@ -216,11 +217,11 @@ func validateRadixRegistrationConfigBranch(rr *v1.RadixRegistration) error { func validateConfigBranch(name string) error { if name == "" { - return ResourceNameCannotBeEmptyError("branch name") + return ResourceNameCannotBeEmptyErrorWithMessage("branch name") } if !branch.IsValidName(name) { - return InvalidConfigBranchName(name) + return InvalidConfigBranchNameWithMessage(name) } return nil @@ -229,14 +230,14 @@ func validateConfigBranch(name string) error { // ValidateRadixConfigFullName Validates the radixconfig file name and path func ValidateRadixConfigFullName(radixConfigFullName string) error { if len(radixConfigFullName) == 0 { - return nil //for empty radixConfigFullName it is used default radixconfig.yaml file name + return nil // for empty radixConfigFullName it is used default radixconfig.yaml file name } matched, err := regexp.Match(radixConfigFullNamePattern, []byte(radixConfigFullName)) if err != nil { return err } if !matched { - return errors.New(invalidRadixConfigFullNameErrorMessage) + return ErrInvalidRadixConfigFullName } return nil } From 482853980d6af352c2c303ecb55ed3d2c79130b4 Mon Sep 17 00:00:00 2001 From: Richard Hagen Date: Wed, 8 Nov 2023 10:25:09 +0100 Subject: [PATCH 2/4] update go mod with pkg/errors (#972) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 91e904111..4b38255b7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/equinor/radix-common v1.5.0 github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 + github.com/pkg/errors v0.9.1 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.1 github.com/prometheus-operator/prometheus-operator/pkg/client v0.64.1 github.com/prometheus/client_golang v1.16.0 @@ -55,7 +56,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect From 2b76aff7712057f480b76529fdd9b105626f8693 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 13:17:34 +0100 Subject: [PATCH 3/4] Bump golang.org/x/net from 0.10.0 to 0.17.0 (#951) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.10.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.10.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 4b38255b7..d69150403 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 - golang.org/x/crypto v0.11.0 + golang.org/x/crypto v0.14.0 golang.org/x/sync v0.2.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.27.6 @@ -66,11 +66,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index de25d2965..491c66727 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -372,8 +372,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -437,11 +437,11 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -451,8 +451,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From f4d006218f82f2d7791695c718001964d1a961db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nils=20Gustav=20Str=C3=A5b=C3=B8?= <65334626+nilsgstrabo@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:02:17 +0100 Subject: [PATCH 4/4] Rebuild changed components only (#965) * update unit test for build enabled components * unfinished build container pipeline * refactory build image and deploy pipline steps * unit tests * fail deploy pipeline when contains components for build * rebuild all components when buildsecret changed * refactor hashing of ra and build secret * refactor pipeline hash func * remove println * update chart * launch.json * handle env namespace or rolebinding not yet created * handle forbidden error for build secret * split setBuildAndDeployImages * refactored target environments in pipeline * reanme pipeline property * fix comment * simplify if statement * fix typo * use magic string for hash when build secret or ra nil * log component image source decisions * fix logging --- .vscode/launch.json | 38 +- Makefile | 2 +- charts/radix-operator/Chart.yaml | 4 +- .../commandbuilder/command.go | 0 .../commandbuilder/command_test.go | 2 +- pipeline-runner/internal/hash/encoding.go | 64 ++ pipeline-runner/internal/hash/hash.go | 62 ++ pipeline-runner/internal/hash/hash_test.go | 82 ++ pipeline-runner/internal/hash/sha256.go | 16 + .../{utils => internal/tekton}/tekton.go | 0 pipeline-runner/{ => internal}/wait/job.go | 0 .../{ => internal}/wait/job_mock.go | 2 +- pipeline-runner/model/pipelineInfo.go | 194 +---- pipeline-runner/model/pipelineInfo_test.go | 25 - pipeline-runner/pipelines/app.go | 2 +- pipeline-runner/steps/apply_radixconfig.go | 368 ++++++++- pipeline-runner/steps/build.go | 23 +- pipeline-runner/steps/build_acr.go | 23 +- pipeline-runner/steps/build_secret.go | 23 +- pipeline-runner/steps/build_test.go | 779 +++++++++++++++++- pipeline-runner/steps/deploy.go | 27 +- pipeline-runner/steps/deploy_test.go | 15 +- pipeline-runner/steps/hash.go | 43 + pipeline-runner/steps/prepare_pipelines.go | 16 +- pipeline-runner/steps/promotion.go | 8 +- pipeline-runner/steps/promotion_test.go | 22 +- pipeline-runner/steps/run_pipelines.go | 12 +- .../applicationconfig/applicationconfig.go | 55 +- .../applicationconfig_test.go | 116 +-- pkg/apis/deployment/deployment.go | 19 +- pkg/apis/deployment/deployment_test.go | 24 +- pkg/apis/deployment/radixcommoncomponent.go | 4 +- pkg/apis/deployment/radixcomponent.go | 4 +- pkg/apis/deployment/radixcomponent_test.go | 242 +++--- pkg/apis/deployment/radixjobcomponent.go | 6 +- pkg/apis/deployment/radixjobcomponent_test.go | 47 +- pkg/apis/deployment/secretrefs_test.go | 6 +- pkg/apis/job/job.go | 4 +- pkg/apis/job/job_steps_test.go | 6 +- pkg/apis/kube/kube.go | 2 + pkg/apis/kube/radix_deployment.go | 8 +- pkg/apis/pipeline/component_image.go | 22 +- pkg/apis/radix/v1/radixapptypes.go | 58 +- pkg/apis/radix/v1/radixdeploytypes.go | 10 + .../radix/v1/radixenvironmentconfigtypes.go | 2 +- radix-operator/application/handler.go | 7 +- 46 files changed, 1803 insertions(+), 691 deletions(-) rename pipeline-runner/{utils => internal}/commandbuilder/command.go (100%) rename pipeline-runner/{utils => internal}/commandbuilder/command_test.go (93%) create mode 100644 pipeline-runner/internal/hash/encoding.go create mode 100644 pipeline-runner/internal/hash/hash.go create mode 100644 pipeline-runner/internal/hash/hash_test.go create mode 100644 pipeline-runner/internal/hash/sha256.go rename pipeline-runner/{utils => internal/tekton}/tekton.go (100%) rename pipeline-runner/{ => internal}/wait/job.go (100%) rename pipeline-runner/{ => internal}/wait/job_mock.go (96%) create mode 100644 pipeline-runner/steps/hash.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 45a5031f4..106183e1c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,22 +10,24 @@ "env": {}, "args": [ "--RADIX_APP=radix-job-demo", - "--JOB_NAME=radix-pipeline-20230321154251-nntcf", + "--JOB_NAME=radix-pipeline-20231030143058-mtwyg", "--PIPELINE_TYPE=build-deploy", "--RADIX_TEKTON_IMAGE=radix-tekton:main-latest", "--RADIX_IMAGE_BUILDER=radix-image-builder:master-latest", + "--RADIX_BUILDAH_IMAGE_BUILDER=quay.io/buildah/stable:v1.31", + "--SECCOMP_PROFILE_FILENAME=allow-buildah.json", "--RADIX_CLUSTER_TYPE=development", "--RADIX_ZONE=dev", - "--RADIX_CLUSTERNAME=weekly-12", + "--RADIX_CLUSTERNAME=weekly-44", "--RADIX_CONTAINER_REGISTRY=radixdev.azurecr.io", "--AZURE_SUBSCRIPTION_ID=16ede44b-1f74-40a5-b428-46cca9a5741b", - "--IMAGE_TAG=nntcf", + "--IMAGE_TAG=abcde", "--BRANCH=main", - "--COMMIT_ID=2bd977cd31687ae97f00b5b7bf0fe44a7608611a", + "--COMMIT_ID=890e19c7bea84678d684daa2a9363cd8be4940bb", "--PUSH_IMAGE=true", "--USE_CACHE=true", "--RADIX_FILE_NAME=/workspace/radixconfig.yaml", - "--TO_ENVIRONMENT=dev", + "--TO_ENVIRONMENT=qa", "--IMAGE_TAG_NAME=server=1.23-alpine-slim", "--IMAGE_TAG_NAME=server2=1.22.1-alpine-perl" ] @@ -38,10 +40,11 @@ "program": "${workspaceFolder}/pipeline-runner/main.go", "env": {}, "args": [ - "--RADIX_APP=radix-github-webhook", + "--RADIX_APP=radix-job-demo", "--IMAGE_TAG=abcdef", "--JOB_NAME=radix-pipeline-promotion-1", "--PIPELINE_TYPE=promote", + "--RADIX_TEKTON_IMAGE=radix-tekton:main-latest", "--FROM_ENVIRONMENT=qa", "--TO_ENVIRONMENT=prod", "--DEPLOYMENT_NAME=qa-etxkt-ac6rxchq", @@ -49,6 +52,27 @@ "--RADIX_CONTAINER_REGISTRY=radixdev.azurecr.io" ] }, + { + "name": "Launch-deploy-pipeline", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/pipeline-runner/main.go", + "env": {}, + "args": [ + "--RADIX_APP=radix-job-demo", + "--JOB_NAME=radix-pipeline-20231030091802-mfzoz", + "--PIPELINE_TYPE=deploy", + "--RADIX_TEKTON_IMAGE=radix-tekton:main-latest", + "--TO_ENVIRONMENT=prod", + "--DEBUG=true", + "--RADIX_FILE_NAME=/workspace/radixconfig.yaml", + "--RADIX_CLUSTER_TYPE=development", + "--RADIX_ZONE=dev", + "--RADIX_CLUSTERNAME=weekly-44", + "--RADIX_CONTAINER_REGISTRY=radixdev.azurecr.io" + ] + }, { "name": "Launch-operator", "type": "go", @@ -73,7 +97,7 @@ "RADIXOPERATOR_APP_ROLLING_UPDATE_MAX_SURGE": "25%", "RADIXOPERATOR_APP_READINESS_PROBE_INITIAL_DELAY_SECONDS": "5", "RADIXOPERATOR_APP_READINESS_PROBE_PERIOD_SECONDS": "10", - "RADIX_ACTIVE_CLUSTERNAME": "weekly-xx", + "RADIX_ACTIVE_CLUSTERNAME": "weekly-45", "RADIX_IMAGE_BUILDER": "radix-image-builder:master-latest", "RADIX_TEKTON_IMAGE": "radix-tekton:main-latest", "RADIXOPERATOR_JOB_SCHEDULER": "radix-job-scheduler:main-latest", diff --git a/Makefile b/Makefile index a3a737e31..ce2f3df15 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ mocks: mockgen -source ./pkg/apis/batch/syncer.go -destination ./pkg/apis/batch/syncer_mock.go -package batch mockgen -source ./pkg/apis/batch/syncerfactory.go -destination ./pkg/apis/batch/syncerfactory_mock.go -package batch mockgen -source ./radix-operator/common/handler.go -destination ./radix-operator/common/handler_mock.go -package common - mockgen -source ./pipeline-runner/wait/job.go -destination ./pipeline-runner/wait/job_mock.go -package wait + mockgen -source ./pipeline-runner/internal/wait/job.go -destination ./pipeline-runner/internal/wait/job_mock.go -package wait .PHONY: build-pipeline build-pipeline: diff --git a/charts/radix-operator/Chart.yaml b/charts/radix-operator/Chart.yaml index e63664c01..8395854d6 100644 --- a/charts/radix-operator/Chart.yaml +++ b/charts/radix-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: radix-operator -version: 1.24.1 -appVersion: 1.44.1 +version: 1.25.0 +appVersion: 1.45.0 kubeVersion: ">=1.24.0" description: Radix Operator keywords: diff --git a/pipeline-runner/utils/commandbuilder/command.go b/pipeline-runner/internal/commandbuilder/command.go similarity index 100% rename from pipeline-runner/utils/commandbuilder/command.go rename to pipeline-runner/internal/commandbuilder/command.go diff --git a/pipeline-runner/utils/commandbuilder/command_test.go b/pipeline-runner/internal/commandbuilder/command_test.go similarity index 93% rename from pipeline-runner/utils/commandbuilder/command_test.go rename to pipeline-runner/internal/commandbuilder/command_test.go index 524a3b01c..d755d3a1f 100644 --- a/pipeline-runner/utils/commandbuilder/command_test.go +++ b/pipeline-runner/internal/commandbuilder/command_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/equinor/radix-operator/pipeline-runner/utils/commandbuilder" + "github.com/equinor/radix-operator/pipeline-runner/internal/commandbuilder" "github.com/stretchr/testify/assert" ) diff --git a/pipeline-runner/internal/hash/encoding.go b/pipeline-runner/internal/hash/encoding.go new file mode 100644 index 000000000..2192540d4 --- /dev/null +++ b/pipeline-runner/internal/hash/encoding.go @@ -0,0 +1,64 @@ +package hash + +import ( + "encoding/binary" + "fmt" + "math" + "reflect" + + yamlk8s "sigs.k8s.io/yaml" +) + +type encoder func(v any) ([]byte, error) + +func boolEncoder(v any) ([]byte, error) { + if reflect.ValueOf(v).Bool() { + return []byte{1}, nil + } + return []byte{0}, nil +} + +func intEncoder(v any) ([]byte, error) { + vt := reflect.ValueOf(v) + return binary.AppendVarint([]byte{}, vt.Int()), nil +} + +func uintEncoder(v any) ([]byte, error) { + vt := reflect.ValueOf(v) + return binary.AppendUvarint([]byte{}, vt.Uint()), nil +} + +func floatEncoder(v any) ([]byte, error) { + vt := reflect.ValueOf(v) + return uintEncoder(math.Float64bits(vt.Float())) +} + +func stringEncoder(v any) ([]byte, error) { + vt := reflect.ValueOf(v) + return []byte(vt.String()), nil +} + +func structEncoder(v any) ([]byte, error) { + b, err := yamlk8s.Marshal(v) + return b, err +} + +func getEncoder(v any) (encoder, error) { + t := reflect.Indirect(reflect.ValueOf(v)) + + switch t.Kind() { + case reflect.Bool: + return boolEncoder, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return intEncoder, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return uintEncoder, nil + case reflect.Float32, reflect.Float64: + return floatEncoder, nil + case reflect.String: + return stringEncoder, nil + case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array: + return structEncoder, nil + } + return nil, fmt.Errorf("encoder for type %s not supported", t.Kind().String()) +} diff --git a/pipeline-runner/internal/hash/hash.go b/pipeline-runner/internal/hash/hash.go new file mode 100644 index 000000000..c532aa3a0 --- /dev/null +++ b/pipeline-runner/internal/hash/hash.go @@ -0,0 +1,62 @@ +package hash + +import ( + "encoding/hex" + "fmt" + "strings" +) + +type Algorithm string + +const ( + hashStringSeparator string = "=" +) + +type sumFunc func(in []byte) []byte + +var sumProviders map[Algorithm]sumFunc = map[Algorithm]sumFunc{} + +func registerProvider(alg Algorithm, provider sumFunc) { + sumProviders[alg] = provider +} + +func ToHashString(alg Algorithm, source any) (string, error) { + sum, found := sumProviders[alg] + if !found { + return "", fmt.Errorf("hash algorithm %s not found", alg) + } + + encode, err := getEncoder(source) + if err != nil { + return "", err + } + + b, err := encode(source) + if err != nil { + return "", err + } + + hashBytes := sum(b) + return joinToHashString(alg, hex.EncodeToString(hashBytes)), nil +} + +func CompareWithHashString(source any, targetHashString string) (bool, error) { + alg := extractAlgorithmFromHashString(targetHashString) + sourceHashString, err := ToHashString(alg, source) + if err != nil { + return false, err + } + return targetHashString == sourceHashString, nil +} + +func joinToHashString(alg Algorithm, hash string) string { + return fmt.Sprintf("%s%s%s", alg, hashStringSeparator, hash) +} + +func extractAlgorithmFromHashString(hashString string) Algorithm { + parts := strings.Split(hashString, hashStringSeparator) + if len(parts) != 2 { + return "" + } + return Algorithm(parts[0]) +} diff --git a/pipeline-runner/internal/hash/hash_test.go b/pipeline-runner/internal/hash/hash_test.go new file mode 100644 index 000000000..572e75c16 --- /dev/null +++ b/pipeline-runner/internal/hash/hash_test.go @@ -0,0 +1,82 @@ +package hash_test + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/equinor/radix-operator/pipeline-runner/internal/hash" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ToHashString(t *testing.T) { + type dataStruct struct { + S string + } + + type algorithm struct { + alg hash.Algorithm + hashLen int + } + algorithms := []algorithm{{alg: hash.SHA256, hashLen: 32}} + + type testSpec struct { + test string + val any + valOther any + match bool + } + + tests := []testSpec{ + {test: "bool match", val: true, valOther: true, match: true}, + {test: "bool no match", val: true, valOther: false, match: false}, + {test: "int match", val: int(10), valOther: int(10), match: true}, + {test: "int no match", val: int(20), valOther: int(21), match: false}, + {test: "int8 match", val: int8(10), valOther: int8(10), match: true}, + {test: "int8 no match", val: int8(20), valOther: int8(21), match: false}, + {test: "int16 match", val: int16(10), valOther: int16(10), match: true}, + {test: "int16 no match", val: int16(20), valOther: int16(21), match: false}, + {test: "int32 match", val: int32(10), valOther: int32(10), match: true}, + {test: "int32 no match", val: int32(20), valOther: int32(21), match: false}, + {test: "int64 match", val: int64(10), valOther: int64(10), match: true}, + {test: "int64 no match", val: int64(20), valOther: int64(21), match: false}, + {test: "uint match", val: uint(10), valOther: uint(10), match: true}, + {test: "uint no match", val: uint(20), valOther: uint(21), match: false}, + {test: "uint8 match", val: uint8(10), valOther: uint8(10), match: true}, + {test: "uint8 no match", val: uint8(20), valOther: uint8(21), match: false}, + {test: "uint16 match", val: uint16(10), valOther: uint16(10), match: true}, + {test: "uint16 no match", val: uint16(20), valOther: uint16(21), match: false}, + {test: "uint32 match", val: uint32(10), valOther: uint32(10), match: true}, + {test: "uint32 no match", val: uint32(20), valOther: uint32(21), match: false}, + {test: "uint64 match", val: uint64(10), valOther: uint64(10), match: true}, + {test: "uint64 no match", val: uint64(20), valOther: uint64(21), match: false}, + {test: "float32 match", val: float32(10.5), valOther: float32(10.5), match: true}, + {test: "float32 no match", val: float32(20.5), valOther: float32(21.5), match: false}, + {test: "float64 match", val: float64(10.5), valOther: float64(10.5), match: true}, + {test: "float64 no match", val: float64(20.5), valOther: float64(21.5), match: false}, + {test: "string match", val: "foo", valOther: "foo", match: true}, + {test: "string no match", val: "foo", valOther: "Foo", match: false}, + {test: "struct match", val: dataStruct{S: "foo"}, valOther: dataStruct{S: "foo"}, match: true}, + {test: "struct no match", val: dataStruct{S: "foo"}, valOther: dataStruct{S: "Foo"}, match: false}, + {test: "struct pointer match", val: dataStruct{S: "foo"}, valOther: &dataStruct{S: "foo"}, match: true}, + } + + for _, test := range tests { + for _, alg := range algorithms { + t.Run(test.test, func(t *testing.T) { + valHash, err := hash.ToHashString(alg.alg, test.val) + require.NoError(t, err) + hashParts := strings.Split(valHash, "=") + assert.Equal(t, string(alg.alg), hashParts[0]) + valHashBytes, err := hex.DecodeString(hashParts[1]) + require.NoError(t, err) + assert.Len(t, valHashBytes, alg.hashLen) + match, err := hash.CompareWithHashString(test.valOther, valHash) + require.NoError(t, err) + assert.Equal(t, test.match, match) + t.Parallel() + }) + } + } +} diff --git a/pipeline-runner/internal/hash/sha256.go b/pipeline-runner/internal/hash/sha256.go new file mode 100644 index 000000000..255be2644 --- /dev/null +++ b/pipeline-runner/internal/hash/sha256.go @@ -0,0 +1,16 @@ +package hash + +import "crypto/sha256" + +const ( + SHA256 Algorithm = "sha256" +) + +func sum256(in []byte) []byte { + hashBytes := sha256.Sum256(in) + return hashBytes[:] +} + +func init() { + registerProvider(SHA256, sum256) +} diff --git a/pipeline-runner/utils/tekton.go b/pipeline-runner/internal/tekton/tekton.go similarity index 100% rename from pipeline-runner/utils/tekton.go rename to pipeline-runner/internal/tekton/tekton.go diff --git a/pipeline-runner/wait/job.go b/pipeline-runner/internal/wait/job.go similarity index 100% rename from pipeline-runner/wait/job.go rename to pipeline-runner/internal/wait/job.go diff --git a/pipeline-runner/wait/job_mock.go b/pipeline-runner/internal/wait/job_mock.go similarity index 96% rename from pipeline-runner/wait/job_mock.go rename to pipeline-runner/internal/wait/job_mock.go index 0bdfbc539..60e4f0bdc 100644 --- a/pipeline-runner/wait/job_mock.go +++ b/pipeline-runner/internal/wait/job_mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./pipeline-runner/wait/job.go +// Source: ./pipeline-runner/internal/wait/job.go // Package wait is a generated GoMock package. package wait diff --git a/pipeline-runner/model/pipelineInfo.go b/pipeline-runner/model/pipelineInfo.go index 083d8ca97..fa41b381c 100644 --- a/pipeline-runner/model/pipelineInfo.go +++ b/pipeline-runner/model/pipelineInfo.go @@ -2,53 +2,44 @@ package model import ( "fmt" - "path/filepath" "strings" "time" + "github.com/equinor/radix-common/utils/slice" "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/utils/conditions" - "github.com/equinor/radix-common/utils/maps" application "github.com/equinor/radix-operator/pkg/apis/applicationconfig" "github.com/equinor/radix-operator/pkg/apis/pipeline" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/securitycontext" "github.com/equinor/radix-operator/pkg/apis/utils" - "github.com/equinor/radix-operator/pkg/apis/utils/git" - log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" ) -const ( - multiComponentImageName = "multi-component" -) - -type componentType struct { - name string - context string - dockerFileName string -} - // PipelineInfo Holds info about the pipeline to run type PipelineInfo struct { Definition *pipeline.Definition - RadixApplication *v1.RadixApplication + RadixApplication *radixv1.RadixApplication + BuildSecret *corev1.Secret PipelineArguments PipelineArguments Steps []Step // Temporary data RadixConfigMapName string GitConfigMapName string - TargetEnvironments map[string]bool - BranchIsMapped bool + TargetEnvironments []string + // GitCommitHash is derived by inspecting HEAD commit after cloning user repository in prepare-pipelines step. // not to be confused with PipelineInfo.PipelineArguments.CommitID GitCommitHash string GitTags string - // Holds information on the images referred to by their respective components - ComponentImages map[string]pipeline.ComponentImage + // Holds information about components to be built + BuildComponentImages pipeline.BuildComponentImages + + // Hold information about components to be deployed + DeployEnvironmentComponentImages pipeline.DeployEnvironmentComponentImages // Prepare pipeline job build context PrepareBuildContext *PrepareBuildContext @@ -186,28 +177,18 @@ func getStepImplementationForStepType(stepType pipeline.StepType, allStepImpleme // SetApplicationConfig Set radixconfig to be used later by other steps, as well // as deriving info from the config func (info *PipelineInfo) SetApplicationConfig(applicationConfig *application.ApplicationConfig) { - ra := applicationConfig.GetRadixApplicationConfig() info.RadixApplication = applicationConfig.GetRadixApplicationConfig() // Obtain metadata for rest of pipeline - branchIsMapped, targetEnvironments := applicationConfig.IsThereAnythingToDeploy(info.PipelineArguments.Branch) + targetEnvironments := applicationConfig.GetTargetEnvironments(info.PipelineArguments.Branch) // For deploy-only pipeline - if info.IsDeployOnlyPipeline() { - targetEnvironments[info.PipelineArguments.ToEnvironment] = true - branchIsMapped = true + if info.IsPipelineType(radixv1.Deploy) && + !slice.Any(targetEnvironments, func(s string) bool { return s == info.PipelineArguments.ToEnvironment }) { + targetEnvironments = append(targetEnvironments, info.PipelineArguments.ToEnvironment) } - info.BranchIsMapped = branchIsMapped info.TargetEnvironments = targetEnvironments - - componentImages := getComponentImages( - ra, - info.PipelineArguments.ContainerRegistry, - info.PipelineArguments.ImageTag, - maps.GetKeysFromMap(targetEnvironments), - info.PipelineArguments.ImageTagNames) - info.ComponentImages = componentImages } // SetGitAttributes Set git attributes to be used later by other steps @@ -216,146 +197,7 @@ func (info *PipelineInfo) SetGitAttributes(gitCommitHash, gitTags string) { info.GitTags = gitTags } -// IsDeployOnlyPipeline Determines if the pipeline is deploy-only -func (info *PipelineInfo) IsDeployOnlyPipeline() bool { - return info.PipelineArguments.ToEnvironment != "" && info.PipelineArguments.FromEnvironment == "" -} - -func getRadixComponentImageSources(ra *v1.RadixApplication, environments []string) []pipeline.ComponentImageSource { - imageSources := make([]pipeline.ComponentImageSource, 0) - - for _, component := range ra.Spec.Components { - if !component.GetEnabledForAnyEnvironment(environments) { - continue - } - imageSource := pipeline.NewComponentImageSourceBuilder(). - WithSourceFunc(pipeline.RadixComponentSource(component)). - Build() - imageSources = append(imageSources, imageSource) - } - - return imageSources -} - -func getRadixJobComponentImageSources(ra *v1.RadixApplication, environments []string) []pipeline.ComponentImageSource { - imageSources := make([]pipeline.ComponentImageSource, 0) - - for _, jobComponent := range ra.Spec.Jobs { - if !jobComponent.GetEnabledForAnyEnvironment(environments) { - continue - } - imageSource := pipeline.NewComponentImageSourceBuilder(). - WithSourceFunc(pipeline.RadixJobComponentSource(jobComponent)). - Build() - imageSources = append(imageSources, imageSource) - } - - return imageSources -} - -func getComponentImages(ra *v1.RadixApplication, containerRegistry, imageTag string, environments []string, imageTagNames map[string]string) map[string]pipeline.ComponentImage { - // Combine components and jobComponents - componentSource := make([]pipeline.ComponentImageSource, 0) - componentSource = append(componentSource, getRadixComponentImageSources(ra, environments)...) - componentSource = append(componentSource, getRadixJobComponentImageSources(ra, environments)...) - - // First check if there are multiple components pointing to the same build context - buildContextComponents := make(map[string][]componentType) - - // To ensure we can iterate over the map in the order - // they were added - buildContextKeys := make([]string, 0) - - for _, c := range componentSource { - if c.Image != "" { - // Using public image. Nothing to build - continue - } - - componentSource := getDockerfile(c.SourceFolder, c.DockerfileName) - components := buildContextComponents[componentSource] - if components == nil { - components = make([]componentType, 0) - buildContextKeys = append(buildContextKeys, componentSource) - } - - components = append(components, componentType{c.Name, getContext(c.SourceFolder), getDockerfileName(c.DockerfileName)}) - buildContextComponents[componentSource] = components - } - - componentImages := make(map[string]pipeline.ComponentImage) - - // Gather pre-built or public images - for _, c := range componentSource { - if c.Image == "" { - continue - } - componentImage := pipeline.ComponentImage{Build: false, ImageName: c.Image, ImagePath: c.Image} - if imageTagNames != nil { - componentImage.ImageTagName = imageTagNames[c.Name] - } - componentImages[c.Name] = componentImage - } - - // Gather build containers - numMultiComponentContainers := 0 - for _, key := range buildContextKeys { - components := buildContextComponents[key] - - var imageName string - - if len(components) > 1 { - log.Infof("Multiple components points to the same build context") - imageName = multiComponentImageName - - if numMultiComponentContainers > 0 { - // Start indexing them - imageName = fmt.Sprintf("%s-%d", imageName, numMultiComponentContainers) - } - - numMultiComponentContainers++ - } else { - imageName = components[0].name - } - - buildContainerName := fmt.Sprintf("build-%s", imageName) - - // A multi-component share context and dockerfile - context := components[0].context - dockerFile := components[0].dockerFileName - - // Set image back to component(s) - for _, c := range components { - componentImages[c.name] = pipeline.ComponentImage{ - ContainerName: buildContainerName, - Context: context, - Dockerfile: dockerFile, - ImageName: imageName, - ImagePath: utils.GetImagePath(containerRegistry, ra.GetName(), imageName, imageTag), - Build: true, - } - } - } - - return componentImages -} - -func getDockerfile(sourceFolder, dockerfileName string) string { - context := getContext(sourceFolder) - dockerfileName = getDockerfileName(dockerfileName) - - return fmt.Sprintf("%s%s", context, dockerfileName) -} - -func getDockerfileName(name string) string { - if name == "" { - name = "Dockerfile" - } - - return name -} - -func getContext(sourceFolder string) string { - sourceRoot := filepath.Join("/", sourceFolder) - return fmt.Sprintf("%s/", filepath.Join(git.Workspace, sourceRoot)) +// IsPipelineType Check pipeline type +func (info *PipelineInfo) IsPipelineType(pipelineType radixv1.RadixPipelineType) bool { + return info.PipelineArguments.PipelineType == string(pipelineType) } diff --git a/pipeline-runner/model/pipelineInfo_test.go b/pipeline-runner/model/pipelineInfo_test.go index 687581509..004e994bc 100644 --- a/pipeline-runner/model/pipelineInfo_test.go +++ b/pipeline-runner/model/pipelineInfo_test.go @@ -177,28 +177,3 @@ func Test_NonExistingPipelineType(t *testing.T) { _, err := pipeline.GetPipelineFromName("non existing pipeline") assert.NotNil(t, err) } - -func Test_IsDeployOnlyPipeline(t *testing.T) { - toEnvironment := "prod" - pipelineArguments := &model.PipelineArguments{ - ToEnvironment: toEnvironment, - } - - pipelineInfo := model.PipelineInfo{ - PipelineArguments: *pipelineArguments, - } - - assert.True(t, pipelineInfo.IsDeployOnlyPipeline()) - - fromEnvironment := "dev" - pipelineArguments = &model.PipelineArguments{ - ToEnvironment: toEnvironment, - FromEnvironment: fromEnvironment, - } - - pipelineInfo = model.PipelineInfo{ - PipelineArguments: *pipelineArguments, - } - - assert.False(t, pipelineInfo.IsDeployOnlyPipeline()) -} diff --git a/pipeline-runner/pipelines/app.go b/pipeline-runner/pipelines/app.go index 777e9c654..6fc490ef6 100644 --- a/pipeline-runner/pipelines/app.go +++ b/pipeline-runner/pipelines/app.go @@ -95,7 +95,7 @@ func (cli *PipelineRunner) TearDown() { log.Errorf("failed on tear-down deleting the config-map %s, ns: %s. %v", cli.pipelineInfo.RadixConfigMapName, namespace, err) } - if cli.pipelineInfo.PipelineArguments.PipelineType == string(v1.BuildDeploy) { + if cli.pipelineInfo.IsPipelineType(v1.BuildDeploy) { err = cli.kubeUtil.DeleteConfigMap(namespace, cli.pipelineInfo.GitConfigMapName) if err != nil && !k8sErrors.IsNotFound(err) { log.Errorf("failed on tear-down deleting the config-map %s, ns: %s. %v", cli.pipelineInfo.GitConfigMapName, namespace, err) diff --git a/pipeline-runner/steps/apply_radixconfig.go b/pipeline-runner/steps/apply_radixconfig.go index 79894d86d..1d3e0d68a 100644 --- a/pipeline-runner/steps/apply_radixconfig.go +++ b/pipeline-runner/steps/apply_radixconfig.go @@ -1,25 +1,38 @@ package steps import ( + "context" + "errors" "fmt" + "path/filepath" "strings" + commonutils "github.com/equinor/radix-common/utils" errorUtils "github.com/equinor/radix-common/utils/errors" + "github.com/equinor/radix-common/utils/slice" "github.com/equinor/radix-operator/pipeline-runner/model" pipelineDefaults "github.com/equinor/radix-operator/pipeline-runner/model/defaults" application "github.com/equinor/radix-operator/pkg/apis/applicationconfig" "github.com/equinor/radix-operator/pkg/apis/defaults" + "github.com/equinor/radix-operator/pkg/apis/kube" "github.com/equinor/radix-operator/pkg/apis/pipeline" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" validate "github.com/equinor/radix-operator/pkg/apis/radixvalidators" - "github.com/equinor/radix-operator/pkg/apis/utils" + operatorutils "github.com/equinor/radix-operator/pkg/apis/utils" + "github.com/equinor/radix-operator/pkg/apis/utils/git" radixclient "github.com/equinor/radix-operator/pkg/client/clientset/versioned" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" + kubeerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" yamlk8s "sigs.k8s.io/yaml" ) +const ( + multiComponentImageName = "multi-component" +) + // ApplyConfigStepImplementation Step to apply RA type ApplyConfigStepImplementation struct { stepType pipeline.StepType @@ -51,14 +64,14 @@ func (cli *ApplyConfigStepImplementation) ErrorMsg(err error) string { // Run Override of default step method func (cli *ApplyConfigStepImplementation) Run(pipelineInfo *model.PipelineInfo) error { // Get pipeline info from configmap created by prepare pipeline step - namespace := utils.GetAppNamespace(cli.GetAppName()) + namespace := operatorutils.GetAppNamespace(cli.GetAppName()) configMap, err := cli.GetKubeutil().GetConfigMap(namespace, pipelineInfo.RadixConfigMapName) if err != nil { return err } // Read build context info from configmap - pipelineInfo.PrepareBuildContext, err = getPrepareBuildContextContent(configMap) + pipelineInfo.PrepareBuildContext, err = getPrepareBuildContext(configMap) if err != nil { return err } @@ -74,21 +87,29 @@ func (cli *ApplyConfigStepImplementation) Run(pipelineInfo *model.PipelineInfo) } // Apply RA to cluster - applicationConfig, err := application.NewApplicationConfig(cli.GetKubeclient(), cli.GetKubeutil(), + applicationConfig := application.NewApplicationConfig(cli.GetKubeclient(), cli.GetKubeutil(), cli.GetRadixclient(), cli.GetRegistration(), ra) - if err != nil { - return err - } err = applicationConfig.ApplyConfigToApplicationNamespace() if err != nil { return err } - // Set back to pipeline pipelineInfo.SetApplicationConfig(applicationConfig) - if pipelineInfo.PipelineArguments.PipelineType == string(v1.BuildDeploy) { + if err := cli.setBuildSecret(pipelineInfo); err != nil { + return err + } + + if err := cli.setBuildAndDeployImages(pipelineInfo); err != nil { + return err + } + + if pipelineInfo.IsPipelineType(radixv1.Deploy) && len(pipelineInfo.BuildComponentImages) > 0 { + return errors.New("deploy pipeline does not support building components and jobs") + } + + if pipelineInfo.IsPipelineType(radixv1.BuildDeploy) { gitCommitHash, gitTags := cli.getHashAndTags(namespace, pipelineInfo) err = validate.GitTagsContainIllegalChars(gitTags) if err != nil { @@ -101,7 +122,324 @@ func (cli *ApplyConfigStepImplementation) Run(pipelineInfo *model.PipelineInfo) return nil } -func getPrepareBuildContextContent(configMap *corev1.ConfigMap) (*model.PrepareBuildContext, error) { +func (cli *ApplyConfigStepImplementation) setBuildSecret(pipelineInfo *model.PipelineInfo) error { + if pipelineInfo.RadixApplication.Spec.Build == nil || len(pipelineInfo.RadixApplication.Spec.Build.Secrets) == 0 { + return nil + } + + secret, err := cli.GetKubeclient().CoreV1().Secrets(operatorutils.GetAppNamespace(cli.GetAppName())).Get(context.TODO(), defaults.BuildSecretsName, metav1.GetOptions{}) + if err != nil { + // For new applications, or when buildsecrets is first added to radixconfig, the secret + // or role bindings may not be synced yet by radix-operator + if kubeerrors.IsNotFound(err) || kubeerrors.IsForbidden(err) { + return nil + } + return err + } + pipelineInfo.BuildSecret = secret + return nil +} + +func (cli *ApplyConfigStepImplementation) setBuildAndDeployImages(pipelineInfo *model.PipelineInfo) error { + componentImageSources, err := getEnvironmentComponentImageSource(pipelineInfo.TargetEnvironments, pipelineInfo.PrepareBuildContext, pipelineInfo.RadixApplication, pipelineInfo.BuildSecret, cli.GetKubeutil()) + if err != nil { + return err + } + distinctComponentsToBuild := getDistinctComponentsToBuild(componentImageSources, pipelineInfo.RadixApplication) + multiComponentDockerfile := getMultiComponentDockerfiles(distinctComponentsToBuild) + pipelineInfo.BuildComponentImages = getBuildComponents(distinctComponentsToBuild, multiComponentDockerfile, pipelineInfo.PipelineArguments.ContainerRegistry, pipelineInfo.PipelineArguments.ImageTag, pipelineInfo.RadixApplication) + pipelineInfo.DeployEnvironmentComponentImages = getEnvironmentDeployComponents(componentImageSources, pipelineInfo.BuildComponentImages, pipelineInfo.PipelineArguments.ImageTagNames) + + if pipelineInfo.IsPipelineType(radixv1.BuildDeploy) { + printEnvironmentComponentImageSources(componentImageSources) + } + + return nil +} + +func printEnvironmentComponentImageSources(imageSources environmentComponentSourceMap) { + log.Info("Component image source in environments:") + for envName, envInfo := range imageSources { + log.Infof(" %s:", envName) + for _, comp := range envInfo.Components { + var imageSource string + switch comp.ImageSource { + case fromDeployment: + imageSource = "active deployment" + case fromBuild: + imageSource = "build" + case fromImagePath: + imageSource = "image in radixconfig" + } + log.Infof(" - %s from %s", comp.ComponentName, imageSource) + } + } +} + +type ( + containerImageSourceEnum int + + componentImageSource struct { + ComponentName string + ImageSource containerImageSourceEnum + } + + environmentComponentSource struct { + RadixApplication *radixv1.RadixApplication + CurrentRadixDeployment *radixv1.RadixDeployment + Components []componentImageSource + } + + environmentComponentSourceMap map[string]environmentComponentSource + + componentDockerFile struct { + ComponentName string + DockerFileFullPath string + } +) + +const ( + fromBuild containerImageSourceEnum = iota + fromImagePath + fromDeployment +) + +// Get component image source for each environment +func getEnvironmentComponentImageSource(targetEnvironments []string, prepareBuildContext *model.PrepareBuildContext, ra *radixv1.RadixApplication, buildSecret *corev1.Secret, kubeUtil *kube.Kube) (environmentComponentSourceMap, error) { + appComponents := getCommonComponents(ra) + + environmentComponents := make(environmentComponentSourceMap) + for _, envName := range targetEnvironments { + envNamespace := operatorutils.GetEnvironmentNamespace(ra.GetName(), envName) + + currentRd, err := getCurrentRadixDeployment(kubeUtil, envNamespace) + if err != nil { + return nil, err + } + + mustBuildComponent, err := mustBuildComponentForEnvironment(envName, prepareBuildContext, currentRd, ra, buildSecret) + if err != nil { + return nil, err + } + + enabledComponents := slice.FindAll(appComponents, func(rcc radixv1.RadixCommonComponent) bool { return rcc.GetEnabledForEnvironment(envName) }) + componentSource := getComponentSources(enabledComponents, mustBuildComponent) + environmentComponents[envName] = environmentComponentSource{ + RadixApplication: ra, + CurrentRadixDeployment: currentRd, + Components: componentSource, + } + } + + return environmentComponents, nil +} + +func getComponentSources(components []radixv1.RadixCommonComponent, mustBuildComponent func(comp radixv1.RadixCommonComponent) bool) []componentImageSource { + componentSource := make([]componentImageSource, 0) + + for _, comp := range components { + var source containerImageSourceEnum + switch { + case len(comp.GetImage()) > 0: + source = fromImagePath + case mustBuildComponent(comp): + source = fromBuild + default: + source = fromDeployment + } + componentSource = append(componentSource, componentImageSource{ComponentName: comp.GetName(), ImageSource: source}) + } + return componentSource +} + +func getCurrentRadixDeployment(kubeUtil *kube.Kube, namespace string) (*radixv1.RadixDeployment, error) { + var currentRd *radixv1.RadixDeployment + // For new applications, or applications with new environments defined in radixconfig, the namespace + // or rolebinding may not be configured yet by radix-operator. + // We skip getting active deployment if namespace does not exist or pipeline-runner does not have access + + if _, err := kubeUtil.KubeClient().CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}); err != nil { + if !kubeerrors.IsNotFound(err) && !kubeerrors.IsForbidden(err) { + return nil, err + } + log.Infof("namespace for environment does not exist yet: %v", err) + } else { + currentRd, err = kubeUtil.GetActiveDeployment(namespace) + if err != nil { + return nil, err + } + } + return currentRd, nil +} + +// Generate list of distinct components to build across all environments +func getDistinctComponentsToBuild(environmentComponentSource environmentComponentSourceMap, ra *radixv1.RadixApplication) []componentDockerFile { + var distinctComponentsToBuild []componentDockerFile + for _, envCompSource := range environmentComponentSource { + distinctComponentsToBuild = slice.Reduce(envCompSource.Components, distinctComponentsToBuild, func(acc []componentDockerFile, cis componentImageSource) []componentDockerFile { + if cis.ImageSource == fromBuild { + if !slice.Any(acc, func(c componentDockerFile) bool { return c.ComponentName == cis.ComponentName }) { + comp := ra.GetCommonComponentByName(cis.ComponentName) + acc = append(acc, componentDockerFile{ComponentName: cis.ComponentName, DockerFileFullPath: getDockerfile(comp.GetSourceFolder(), comp.GetDockerfileName())}) + } + } + return acc + }) + } + return distinctComponentsToBuild +} + +// Generate list of dockerfiles used by more than one component/job (multi-component builds) +func getMultiComponentDockerfiles(componentDockerFiles []componentDockerFile) []string { + return slice.Reduce(componentDockerFiles, []string{}, func(acc []string, cdf componentDockerFile) []string { + dockerFileCount := len(slice.FindAll(componentDockerFiles, func(c componentDockerFile) bool { return c.DockerFileFullPath == cdf.DockerFileFullPath })) + if dockerFileCount > 1 && !slice.Any(acc, func(s string) bool { return s == cdf.DockerFileFullPath }) { + acc = append(acc, cdf.DockerFileFullPath) + } + return acc + }) +} + +// Get component build information used by build job +func getBuildComponents(componentsDockerFile []componentDockerFile, multiComponentDockerfiles []string, containerRegistry, imageTag string, ra *radixv1.RadixApplication) pipeline.BuildComponentImages { + return slice.Reduce(componentsDockerFile, make(pipeline.BuildComponentImages), func(acc pipeline.BuildComponentImages, c componentDockerFile) pipeline.BuildComponentImages { + cc := ra.GetCommonComponentByName(c.ComponentName) + imageName := c.ComponentName + if multiComponentIdx := slice.FindIndex(multiComponentDockerfiles, func(s string) bool { return s == c.DockerFileFullPath }); multiComponentIdx >= 0 { + var suffix string + if multiComponentIdx > 0 { + suffix = fmt.Sprintf("-%d", multiComponentIdx) + } + imageName = fmt.Sprintf("%s%s", multiComponentImageName, suffix) + + } + acc[c.ComponentName] = pipeline.BuildComponentImage{ + ContainerName: fmt.Sprintf("build-%s", imageName), + Context: getContext(cc.GetSourceFolder()), + Dockerfile: getDockerfileName(cc.GetDockerfileName()), + ImageName: imageName, + ImagePath: operatorutils.GetImagePath(containerRegistry, ra.GetName(), imageName, imageTag), + } + return acc + }) +} + +// Get information about components and image to use for each environment when creating RadixDeployments +func getEnvironmentDeployComponents(environmentComponentSource environmentComponentSourceMap, buildComponentImages pipeline.BuildComponentImages, imageTagNames map[string]string) pipeline.DeployEnvironmentComponentImages { + environmentDeployComponentImages := make(pipeline.DeployEnvironmentComponentImages) + for envName, c := range environmentComponentSource { + environmentDeployComponentImages[envName] = slice.Reduce(c.Components, make(pipeline.DeployComponentImages), func(acc pipeline.DeployComponentImages, cis componentImageSource) pipeline.DeployComponentImages { + var imagePath string + switch cis.ImageSource { + case fromBuild: + imagePath = buildComponentImages[cis.ComponentName].ImagePath + case fromDeployment: + imagePath = c.CurrentRadixDeployment.GetCommonComponentByName(cis.ComponentName).GetImage() + case fromImagePath: + imagePath = c.RadixApplication.GetCommonComponentByName(cis.ComponentName).GetImage() + } + + acc[cis.ComponentName] = pipeline.DeployComponentImage{ + ImagePath: imagePath, + ImageTagName: imageTagNames[cis.ComponentName], + Build: cis.ImageSource == fromBuild, + } + return acc + }) + } + + return environmentDeployComponentImages +} + +func isRadixConfigNewOrModifiedSinceDeployment(rd *radixv1.RadixDeployment, ra *radixv1.RadixApplication) (bool, error) { + if rd == nil { + return true, nil + } + currentRdConfigHash := rd.GetAnnotations()[kube.RadixConfigHash] + if len(currentRdConfigHash) == 0 { + return true, nil + } + hashEqual, err := compareRadixApplicationHash(currentRdConfigHash, ra) + if !hashEqual && err != nil { + log.Infof("RadixApplication updated since last deployment to environment %s", rd.Spec.Environment) + } + return !hashEqual, err +} + +func isBuildSecretNewOrModifiedSinceDeployment(rd *radixv1.RadixDeployment, buildSecret *corev1.Secret) (bool, error) { + if rd == nil { + return true, nil + } + targetHash := rd.GetAnnotations()[kube.RadixBuildSecretHash] + if len(targetHash) == 0 { + return true, nil + } + hashEqual, err := compareBuildSecretHash(targetHash, buildSecret) + if !hashEqual && err != nil { + log.Infof("Build secrets updated since last deployment to environment %s", rd.Spec.Environment) + } + return !hashEqual, err +} + +func mustBuildComponentForEnvironment(environmentName string, prepareBuildContext *model.PrepareBuildContext, currentRd *radixv1.RadixDeployment, ra *radixv1.RadixApplication, buildSecret *corev1.Secret) (func(comp radixv1.RadixCommonComponent) bool, error) { + alwaysBuild := func(comp radixv1.RadixCommonComponent) bool { + return true + } + + if prepareBuildContext == nil || currentRd == nil { + return alwaysBuild, nil + } + + if isModified, err := isRadixConfigNewOrModifiedSinceDeployment(currentRd, ra); err != nil { + return nil, err + } else if isModified { + return alwaysBuild, nil + } + + if isModified, err := isBuildSecretNewOrModifiedSinceDeployment(currentRd, buildSecret); err != nil { + return nil, err + } else if isModified { + return alwaysBuild, nil + } + + envBuildContext, found := slice.FindFirst(prepareBuildContext.EnvironmentsToBuild, func(etb model.EnvironmentToBuild) bool { return etb.Environment == environmentName }) + if !found { + return alwaysBuild, nil + } + + return func(comp radixv1.RadixCommonComponent) bool { + return slice.Any(envBuildContext.Components, func(s string) bool { return s == comp.GetName() }) || + commonutils.IsNil(currentRd.GetCommonComponentByName(comp.GetName())) + }, nil +} + +func getDockerfile(sourceFolder, dockerfileName string) string { + context := getContext(sourceFolder) + dockerfileName = getDockerfileName(dockerfileName) + + return fmt.Sprintf("%s%s", context, dockerfileName) +} + +func getDockerfileName(name string) string { + if name == "" { + name = "Dockerfile" + } + + return name +} + +func getContext(sourceFolder string) string { + sourceRoot := filepath.Join("/", sourceFolder) + return fmt.Sprintf("%s/", filepath.Join(git.Workspace, sourceRoot)) +} + +func getCommonComponents(ra *radixv1.RadixApplication) []radixv1.RadixCommonComponent { + commonComponents := slice.Map(ra.Spec.Components, func(c radixv1.RadixComponent) radixv1.RadixCommonComponent { return &c }) + commonComponents = append(commonComponents, slice.Map(ra.Spec.Jobs, func(c radixv1.RadixJobComponent) radixv1.RadixCommonComponent { return &c })...) + return commonComponents +} + +func getPrepareBuildContext(configMap *corev1.ConfigMap) (*model.PrepareBuildContext, error) { prepareBuildContextContent, ok := configMap.Data[pipelineDefaults.PipelineConfigMapBuildContext] if !ok { log.Debug("Prepare Build Context does not exist in the ConfigMap") @@ -140,10 +478,10 @@ func printPrepareBuildContext(prepareBuildContext *model.PrepareBuildContext) { log.Infoln("Radix config file was changed in the repository") } if len(prepareBuildContext.EnvironmentsToBuild) > 0 { - log.Infoln("Components to build in environments:") + log.Infoln("Components with changed source code in environments:") for _, environmentToBuild := range prepareBuildContext.EnvironmentsToBuild { if len(environmentToBuild.Components) == 0 { - log.Infof(" - %s: no components or jobs with changed source", environmentToBuild.Environment) + log.Infof(" - %s: no components or jobs with changed source code", environmentToBuild.Environment) } else { log.Infof(" - %s: %s", environmentToBuild.Environment, strings.Join(environmentToBuild.Components, ",")) } @@ -177,8 +515,8 @@ func (cli *ApplyConfigStepImplementation) getHashAndTags(namespace string, pipel // CreateRadixApplication Create RadixApplication from radixconfig.yaml content func CreateRadixApplication(radixClient radixclient.Interface, - configFileContent string) (*v1.RadixApplication, error) { - ra := &v1.RadixApplication{} + configFileContent string) (*radixv1.RadixApplication, error) { + ra := &radixv1.RadixApplication{} // Important: Must use sigs.k8s.io/yaml decoder to correctly unmarshal Kubernetes objects. // This package supports encoding and decoding of yaml for CRD struct types using the json tag. diff --git a/pipeline-runner/steps/build.go b/pipeline-runner/steps/build.go index 88a907621..9934a8492 100644 --- a/pipeline-runner/steps/build.go +++ b/pipeline-runner/steps/build.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + internalwait "github.com/equinor/radix-operator/pipeline-runner/internal/wait" "github.com/equinor/radix-operator/pipeline-runner/model" - pipelinewait "github.com/equinor/radix-operator/pipeline-runner/wait" jobUtil "github.com/equinor/radix-operator/pkg/apis/job" "github.com/equinor/radix-operator/pkg/apis/kube" "github.com/equinor/radix-operator/pkg/apis/pipeline" @@ -22,12 +22,12 @@ import ( type BuildStepImplementation struct { stepType pipeline.StepType model.DefaultStepImplementation - jobWaiter pipelinewait.JobCompletionWaiter + jobWaiter internalwait.JobCompletionWaiter } // NewBuildStep Constructor. // jobWaiter is optional and will be set by Init(...) function if nil. -func NewBuildStep(jobWaiter pipelinewait.JobCompletionWaiter) model.Step { +func NewBuildStep(jobWaiter internalwait.JobCompletionWaiter) model.Step { step := &BuildStepImplementation{ stepType: pipeline.BuildStep, jobWaiter: jobWaiter, @@ -39,7 +39,7 @@ func NewBuildStep(jobWaiter pipelinewait.JobCompletionWaiter) model.Step { func (step *BuildStepImplementation) Init(kubeclient kubernetes.Interface, radixclient radixclient.Interface, kubeutil *kube.Kube, prometheusOperatorClient monitoring.Interface, rr *v1.RadixRegistration) { step.DefaultStepImplementation.Init(kubeclient, radixclient, kubeutil, prometheusOperatorClient, rr) if step.jobWaiter == nil { - step.jobWaiter = pipelinewait.NewJobCompletionWaiter(kubeclient) + step.jobWaiter = internalwait.NewJobCompletionWaiter(kubeclient) } } @@ -63,12 +63,12 @@ func (cli *BuildStepImplementation) Run(pipelineInfo *model.PipelineInfo) error branch := pipelineInfo.PipelineArguments.Branch commitID := pipelineInfo.GitCommitHash - if !pipelineInfo.BranchIsMapped { + if len(pipelineInfo.TargetEnvironments) == 0 { log.Infof("Skip build step as branch %s is not mapped to any environment", pipelineInfo.PipelineArguments.Branch) return nil } - if !needToBuildComponents(pipelineInfo.ComponentImages) { + if len(pipelineInfo.BuildComponentImages) == 0 { log.Infof("No component in app %s requires building", cli.GetAppName()) return nil } @@ -76,7 +76,7 @@ func (cli *BuildStepImplementation) Run(pipelineInfo *model.PipelineInfo) error log.Infof("Building app %s for branch %s and commit %s", cli.GetAppName(), branch, commitID) namespace := utils.GetAppNamespace(cli.GetAppName()) - buildSecrets, err := getBuildSecretsAsVariables(cli.GetKubeclient(), pipelineInfo.RadixApplication, namespace) + buildSecrets, err := getBuildSecretsAsVariables(pipelineInfo) if err != nil { return err } @@ -104,12 +104,3 @@ func (cli *BuildStepImplementation) Run(pipelineInfo *model.PipelineInfo) error return cli.jobWaiter.Wait(job) } - -func needToBuildComponents(componentImages map[string]pipeline.ComponentImage) bool { - for _, componentImage := range componentImages { - if componentImage.Build { - return true - } - } - return false -} diff --git a/pipeline-runner/steps/build_acr.go b/pipeline-runner/steps/build_acr.go index 7a0b377f9..66a123954 100644 --- a/pipeline-runner/steps/build_acr.go +++ b/pipeline-runner/steps/build_acr.go @@ -6,8 +6,8 @@ import ( "strings" "time" + "github.com/equinor/radix-operator/pipeline-runner/internal/commandbuilder" "github.com/equinor/radix-operator/pipeline-runner/model" - "github.com/equinor/radix-operator/pipeline-runner/utils/commandbuilder" "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/kube" v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" @@ -39,7 +39,7 @@ func createACRBuildJob(rr *v1.RadixRegistration, pipelineInfo *model.PipelineInf timestamp := time.Now().Format("20060102150405") defaultMode, backOffLimit := int32(256), int32(0) - componentImagesAnnotation, _ := json.Marshal(pipelineInfo.ComponentImages) + componentImagesAnnotation, _ := json.Marshal(pipelineInfo.BuildComponentImages) hash := strings.ToLower(utils.RandStringStrSeed(5, pipelineInfo.PipelineArguments.JobName)) annotations := radixannotations.ForClusterAutoscalerSafeToEvict(false) buildPodSecurityContext := &pipelineInfo.PipelineArguments.PodSecurityContext @@ -138,7 +138,7 @@ func createACRBuildContainers(appName string, pipelineInfo *model.PipelineInfo, imageBuilder := fmt.Sprintf("%s/%s", containerRegistry, pipelineInfo.PipelineArguments.ImageBuilder) subscriptionId := pipelineInfo.PipelineArguments.SubscriptionId branch := pipelineInfo.PipelineArguments.Branch - targetEnvs := strings.Join(getTargetEnvsToBuild(pipelineInfo), ",") + targetEnvs := strings.Join(pipelineInfo.TargetEnvironments, ",") secretMountsArgsString := "" if isUsingBuildKit(pipelineInfo) { @@ -162,12 +162,7 @@ func createACRBuildContainers(appName string, pipelineInfo *model.PipelineInfo, useCache = "--no-cache" } distinctBuildContainers := make(map[string]void) - for _, componentImage := range pipelineInfo.ComponentImages { - if !componentImage.Build { - // Nothing to build - continue - } - + for _, componentImage := range pipelineInfo.BuildComponentImages { if _, exists := distinctBuildContainers[componentImage.ContainerName]; exists { // We already have a container for this multi-component continue @@ -412,13 +407,3 @@ func getSecretArgs(buildSecrets []corev1.EnvVar) string { } return strings.Join(secretArgs, " ") } - -func getTargetEnvsToBuild(pipelineInfo *model.PipelineInfo) []string { - var envs []string - for env, toBuild := range pipelineInfo.TargetEnvironments { - if toBuild { - envs = append(envs, env) - } - } - return envs -} diff --git a/pipeline-runner/steps/build_secret.go b/pipeline-runner/steps/build_secret.go index 144dc4223..1460ef4c5 100644 --- a/pipeline-runner/steps/build_secret.go +++ b/pipeline-runner/steps/build_secret.go @@ -1,35 +1,32 @@ package steps import ( - "context" "errors" "fmt" + "strings" + + "github.com/equinor/radix-operator/pipeline-runner/model" "github.com/equinor/radix-operator/pkg/apis/defaults" - v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - "strings" ) // Will ensure that all build secrets are mounted from build-secrets secret with BUILD_SECRET_ prefix -func getBuildSecretsAsVariables(kubeclient kubernetes.Interface, ra *v1.RadixApplication, appNamespace string) ([]corev1.EnvVar, error) { - if ra.Spec.Build == nil || len(ra.Spec.Build.Secrets) == 0 { +func getBuildSecretsAsVariables(pipelineInfo *model.PipelineInfo) ([]corev1.EnvVar, error) { + if pipelineInfo.RadixApplication.Spec.Build == nil || len(pipelineInfo.RadixApplication.Spec.Build.Secrets) == 0 { return nil, nil } - var environmentVariables []corev1.EnvVar - buildSecrets, err := kubeclient.CoreV1().Secrets(appNamespace).Get(context.TODO(), defaults.BuildSecretsName, metav1.GetOptions{}) - if err != nil || buildSecrets == nil { + if pipelineInfo.BuildSecret == nil { return nil, errors.New("build secrets has not been set") } - for _, secretName := range ra.Spec.Build.Secrets { - if _, ok := buildSecrets.Data[secretName]; !ok { + var environmentVariables []corev1.EnvVar + for _, secretName := range pipelineInfo.RadixApplication.Spec.Build.Secrets { + if _, ok := pipelineInfo.BuildSecret.Data[secretName]; !ok { return nil, fmt.Errorf("build secret %s has not been set", secretName) } - secretValue := string(buildSecrets.Data[secretName]) + secretValue := string(pipelineInfo.BuildSecret.Data[secretName]) if strings.EqualFold(secretValue, defaults.BuildSecretDefaultData) { return nil, fmt.Errorf("build secret %s has not been set", secretName) } diff --git a/pipeline-runner/steps/build_test.go b/pipeline-runner/steps/build_test.go index b821d9d52..9334ce61c 100644 --- a/pipeline-runner/steps/build_test.go +++ b/pipeline-runner/steps/build_test.go @@ -5,13 +5,15 @@ import ( "fmt" "strings" "testing" + "time" "github.com/equinor/radix-common/utils/pointers" "github.com/equinor/radix-common/utils/slice" + "github.com/equinor/radix-operator/pipeline-runner/internal/hash" + internalwait "github.com/equinor/radix-operator/pipeline-runner/internal/wait" "github.com/equinor/radix-operator/pipeline-runner/model" pipelineDefaults "github.com/equinor/radix-operator/pipeline-runner/model/defaults" "github.com/equinor/radix-operator/pipeline-runner/steps" - pipelinewait "github.com/equinor/radix-operator/pipeline-runner/wait" application "github.com/equinor/radix-operator/pkg/apis/applicationconfig" "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/kube" @@ -19,10 +21,12 @@ import ( "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/equinor/radix-operator/pkg/apis/utils/annotations" "github.com/equinor/radix-operator/pkg/apis/utils/git" + "github.com/equinor/radix-operator/pkg/apis/utils/labels" radixfake "github.com/equinor/radix-operator/pkg/client/clientset/versioned/fake" "github.com/golang/mock/gomock" prometheusfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kubefake "k8s.io/client-go/kubernetes/fake" @@ -50,6 +54,14 @@ func (s *buildTestSuite) SetupTest() { s.ctrl = gomock.NewController(s.T()) } +func (s *buildTestSuite) SetupSubTest() { + s.kubeClient = kubefake.NewSimpleClientset() + s.radixClient = radixfake.NewSimpleClientset() + s.promClient = prometheusfake.NewSimpleClientset() + s.kubeUtil, _ = kube.New(s.kubeClient, s.radixClient, nil) + s.ctrl = gomock.NewController(s.T()) +} + func (s *buildTestSuite) Test_BranchIsNotMapped_ShouldSkip() { anyBranch := "master" anyEnvironment := "dev" @@ -68,13 +80,13 @@ func (s *buildTestSuite) Test_BranchIsNotMapped_ShouldSkip() { WithName(anyComponentName)). BuildRA() - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(0) cli := steps.NewBuildStep(jobWaiter) cli.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - applicationConfig, _ := application.NewApplicationConfig(s.kubeClient, s.kubeUtil, s.radixClient, rr, ra) - branchIsMapped, targetEnvs := applicationConfig.IsThereAnythingToDeploy(anyNoMappedBranch) + applicationConfig := application.NewApplicationConfig(s.kubeClient, s.kubeUtil, s.radixClient, rr, ra) + targetEnvs := applicationConfig.GetTargetEnvironments(anyNoMappedBranch) pipelineInfo := &model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ @@ -84,7 +96,6 @@ func (s *buildTestSuite) Test_BranchIsNotMapped_ShouldSkip() { CommitID: anyCommitID, }, TargetEnvironments: targetEnvs, - BranchIsMapped: branchIsMapped, } err := cli.Run(pipelineInfo) @@ -108,7 +119,7 @@ func (s *buildTestSuite) Test_BuildDeploy_JobSpecAndDeploymentConsistent() { WithEnvironment("prod", "release"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) s.Require().NoError(s.createGitInfoConfigMapResponse(gitConfigMapName, appName, gitHash, gitTags)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ @@ -132,7 +143,7 @@ func (s *buildTestSuite) Test_BuildDeploy_JobSpecAndDeploymentConsistent() { applyStep := steps.NewApplyConfigStep() applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) @@ -206,6 +217,11 @@ func (s *buildTestSuite) Test_BuildDeploy_JobSpecAndDeploymentConsistent() { rds, _ := s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).List(context.Background(), metav1.ListOptions{}) s.Require().Len(rds.Items, 1) rd := rds.Items[0] + expectedRaHash, err := hash.ToHashString(hash.SHA256, ra.Spec) + s.Require().NoError(err) + s.Equal(expectedRaHash, rd.GetAnnotations()[kube.RadixConfigHash]) + s.Equal(s.getBuildSecretHash(nil), rd.GetAnnotations()[kube.RadixBuildSecretHash]) + s.Greater(len(rd.GetAnnotations()[kube.RadixConfigHash]), 0) s.Require().Len(rd.Spec.Components, 1) s.Equal(compName, rd.Spec.Components[0].Name) s.Equal(fmt.Sprintf("%s/%s-%s:%s", pipeline.PipelineArguments.ContainerRegistry, appName, compName, pipeline.PipelineArguments.ImageTag), rd.Spec.Components[0].Image) @@ -243,7 +259,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents() { utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("public-job-component").WithImage("job/job:latest"), ). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ PipelineType: "build-deploy", @@ -259,7 +275,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents() { applyStep := steps.NewApplyConfigStep() applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) @@ -365,6 +381,10 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("client-component-3").WithEnabled(false).WithSourceFolder("./client/").WithDockerfileName("client.Dockerfile"), utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("client-component-4").WithSourceFolder("./client2/").WithDockerfileName("client.Dockerfile"), utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("client-component-5").WithEnabled(false).WithSourceFolder("./client2/").WithDockerfileName("client.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("client-component-6").WithEnabled(false).WithSourceFolder("./client3/").WithDockerfileName("client.Dockerfile"). + WithEnvironmentConfig(utils.NewComponentEnvironmentBuilder().WithEnvironment(envName).WithEnabled(true)), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("client-component-7").WithEnabled(true).WithSourceFolder("./client4/").WithDockerfileName("client.Dockerfile"). + WithEnvironmentConfig(utils.NewComponentEnvironmentBuilder().WithEnvironment(envName).WithEnabled(false)), ). WithJobComponents( utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("calc-1").WithDockerfileName("calc.Dockerfile").WithSourceFolder("./calc/"), @@ -372,9 +392,13 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("calc-3").WithEnabled(false).WithDockerfileName("calc.Dockerfile").WithSourceFolder("./calc/"), utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("calc-4").WithDockerfileName("calc.Dockerfile").WithSourceFolder("./calc2/"), utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("calc-5").WithEnabled(false).WithDockerfileName("calc.Dockerfile").WithSourceFolder("./calc2/"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("calc-6").WithEnabled(false).WithDockerfileName("calc.Dockerfile").WithSourceFolder("./calc3/"). + WithEnvironmentConfig(utils.NewJobComponentEnvironmentBuilder().WithEnvironment(envName).WithEnabled(true)), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("calc-7").WithEnabled(true).WithDockerfileName("calc.Dockerfile").WithSourceFolder("./calc4/"). + WithEnvironmentConfig(utils.NewJobComponentEnvironmentBuilder().WithEnvironment(envName).WithEnabled(false)), ). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ PipelineType: "build-deploy", @@ -390,7 +414,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { applyStep := steps.NewApplyConfigStep() applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) @@ -418,7 +442,9 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { {Name: "build-multi-component", Docker: "client.Dockerfile", Context: "/workspace/client/", Image: imageNameFunc("multi-component")}, {Name: "build-multi-component-1", Docker: "calc.Dockerfile", Context: "/workspace/calc/", Image: imageNameFunc("multi-component-1")}, {Name: "build-client-component-4", Docker: "client.Dockerfile", Context: "/workspace/client2/", Image: imageNameFunc("client-component-4")}, + {Name: "build-client-component-6", Docker: "client.Dockerfile", Context: "/workspace/client3/", Image: imageNameFunc("client-component-6")}, {Name: "build-calc-4", Docker: "calc.Dockerfile", Context: "/workspace/calc2/", Image: imageNameFunc("calc-4")}, + {Name: "build-calc-6", Docker: "calc.Dockerfile", Context: "/workspace/calc3/", Image: imageNameFunc("calc-6")}, } actualJobContainers := slice.Map(job.Spec.Template.Spec.Containers, func(c corev1.Container) jobContainerSpec { getEnv := func(env string) string { @@ -448,6 +474,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { {Name: "client-component-1", Image: imageNameFunc("multi-component")}, {Name: "client-component-2", Image: imageNameFunc("multi-component")}, {Name: "client-component-4", Image: imageNameFunc("client-component-4")}, + {Name: "client-component-6", Image: imageNameFunc("client-component-6")}, } actualDeployComponents := slice.Map(rd.Spec.Components, func(c radixv1.RadixDeployComponent) deployComponentSpec { return deployComponentSpec{Name: c.Name, Image: c.Image} @@ -457,6 +484,657 @@ func (s *buildTestSuite) Test_BuildJobSpec_MultipleComponents_IgnoreDisabled() { {Name: "calc-1", Image: imageNameFunc("multi-component-1")}, {Name: "calc-2", Image: imageNameFunc("multi-component-1")}, {Name: "calc-4", Image: imageNameFunc("calc-4")}, + {Name: "calc-6", Image: imageNameFunc("calc-6")}, + } + actualJobComponents := slice.Map(rd.Spec.Jobs, func(c radixv1.RadixDeployJobComponent) deployComponentSpec { + return deployComponentSpec{Name: c.Name, Image: c.Image} + }) + s.ElementsMatch(expectedJobComponents, actualJobComponents) +} + +func (s *buildTestSuite) Test_BuildChangedComponents() { + appName, envName, rjName, buildBranch, jobPort := "anyapp", "dev", "anyrj", "anybranch", pointers.Ptr[int32](9999) + prepareConfigMapName := "preparecm" + + rr := utils.NewRegistrationBuilder().WithName(appName).BuildRR() + _, _ = s.radixClient.RadixV1().RadixRegistrations().Create(context.Background(), rr, metav1.CreateOptions{}) + rj := utils.ARadixBuildDeployJob().WithJobName(rjName).WithAppName(appName).BuildRJ() + _, _ = s.radixClient.RadixV1().RadixJobs(utils.GetAppNamespace(appName)).Create(context.Background(), rj, metav1.CreateOptions{}) + ra := utils.NewRadixApplicationBuilder(). + WithAppName(appName). + WithEnvironment(envName, buildBranch). + WithComponents( + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-changed").WithDockerfileName("comp-changed.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-new").WithDockerfileName("comp-new.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-unchanged").WithDockerfileName("comp-unchanged.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-common1-changed").WithDockerfileName("common1.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-common2-unchanged").WithDockerfileName("common2.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-common3-changed").WithDockerfileName("common3.Dockerfile"), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp-deployonly").WithImage("comp-deployonly:anytag"), + ). + WithJobComponents( + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-changed").WithDockerfileName("job-changed.Dockerfile"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-new").WithDockerfileName("job-new.Dockerfile"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-unchanged").WithDockerfileName("job-unchanged.Dockerfile"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-common1-unchanged").WithDockerfileName("common1.Dockerfile"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-common2-changed").WithDockerfileName("common2.Dockerfile"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-common3-changed").WithDockerfileName("common3.Dockerfile"), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job-deployonly").WithImage("job-deployonly:anytag"), + ). + BuildRA() + currentRd := utils.NewDeploymentBuilder(). + WithDeploymentName("currentrd"). + WithAppName(appName). + WithEnvironment(envName). + WithAnnotations(map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(ra), kube.RadixBuildSecretHash: s.getBuildSecretHash(nil)}). + WithCondition(radixv1.DeploymentActive). + WithComponents( + utils.NewDeployComponentBuilder().WithName("comp-changed").WithImage("comp-changed-current:anytag"), + utils.NewDeployComponentBuilder().WithName("comp-unchanged").WithImage("comp-unchanged-current:anytag"), + utils.NewDeployComponentBuilder().WithName("comp-common1-changed").WithImage("comp-common1-changed:anytag"), + utils.NewDeployComponentBuilder().WithName("comp-common2-unchanged").WithImage("comp-common2-unchanged:anytag"), + ). + WithJobComponents( + utils.NewDeployJobComponentBuilder().WithName("job-changed").WithImage("job-changed-current:anytag"), + utils.NewDeployJobComponentBuilder().WithName("job-unchanged").WithImage("job-unchanged-current:anytag"), + utils.NewDeployJobComponentBuilder().WithName("job-common1-unchanged").WithImage("job-common1-unchanged:anytag"), + utils.NewDeployJobComponentBuilder().WithName("job-common2-changed").WithImage("job-common2-changed:anytag"), + ). + BuildRD() + _, _ = s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).Create(context.Background(), currentRd, metav1.CreateOptions{}) + _, _ = s.kubeClient.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: utils.GetEnvironmentNamespace(appName, envName)}}, metav1.CreateOptions{}) + buildCtx := &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + {Environment: envName, Components: []string{"comp-changed", "comp-common1-changed", "comp-common3-changed", "job-changed", "job-common2-changed", "job-common3-changed"}}, + }, + } + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, buildCtx)) + pipeline := model.PipelineInfo{ + PipelineArguments: model.PipelineArguments{ + PipelineType: "build-deploy", + Branch: buildBranch, + JobName: rjName, + ImageTag: "imgtag", + ContainerRegistry: "registry", + Clustertype: "clustertype", + Clustername: "clustername", + }, + RadixConfigMapName: prepareConfigMapName, + } + + applyStep := steps.NewApplyConfigStep() + applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) + buildStep := steps.NewBuildStep(jobWaiter) + buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + + s.Require().NoError(applyStep.Run(&pipeline)) + s.Require().NoError(buildStep.Run(&pipeline)) + s.Require().NoError(deployStep.Run(&pipeline)) + jobs, _ := s.kubeClient.BatchV1().Jobs(utils.GetAppNamespace(appName)).List(context.Background(), metav1.ListOptions{}) + s.Require().Len(jobs.Items, 1) + job := jobs.Items[0] + + // Check build containers + imageNameFunc := func(s string) string { + return fmt.Sprintf("%s/%s-%s:%s", pipeline.PipelineArguments.ContainerRegistry, appName, s, pipeline.PipelineArguments.ImageTag) + } + expectedJobContainers := []string{ + "build-comp-changed", + "build-comp-new", + "build-job-changed", + "build-job-new", + "build-comp-common1-changed", + "build-job-common2-changed", + "build-multi-component", + } + actualJobContainers := slice.Map(job.Spec.Template.Spec.Containers, func(c corev1.Container) string { return c.Name }) + s.ElementsMatch(expectedJobContainers, actualJobContainers) + + // Check RadixDeployment component and job images + rds, _ := s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).List(context.Background(), metav1.ListOptions{LabelSelector: labels.ForPipelineJobName(rjName).String()}) + s.Require().Len(rds.Items, 1) + rd := rds.Items[0] + type deployComponentSpec struct { + Name string + Image string + } + expectedDeployComponents := []deployComponentSpec{ + {Name: "comp-changed", Image: imageNameFunc("comp-changed")}, + {Name: "comp-new", Image: imageNameFunc("comp-new")}, + {Name: "comp-unchanged", Image: "comp-unchanged-current:anytag"}, + {Name: "comp-deployonly", Image: "comp-deployonly:anytag"}, + {Name: "comp-common1-changed", Image: imageNameFunc("comp-common1-changed")}, + {Name: "comp-common2-unchanged", Image: "comp-common2-unchanged:anytag"}, + {Name: "comp-common3-changed", Image: imageNameFunc("multi-component")}, + } + actualDeployComponents := slice.Map(rd.Spec.Components, func(c radixv1.RadixDeployComponent) deployComponentSpec { + return deployComponentSpec{Name: c.Name, Image: c.Image} + }) + s.ElementsMatch(expectedDeployComponents, actualDeployComponents) + expectedJobComponents := []deployComponentSpec{ + {Name: "job-changed", Image: imageNameFunc("job-changed")}, + {Name: "job-new", Image: imageNameFunc("job-new")}, + {Name: "job-unchanged", Image: "job-unchanged-current:anytag"}, + {Name: "job-deployonly", Image: "job-deployonly:anytag"}, + {Name: "job-common1-unchanged", Image: "job-common1-unchanged:anytag"}, + {Name: "job-common2-changed", Image: imageNameFunc("job-common2-changed")}, + {Name: "job-common3-changed", Image: imageNameFunc("multi-component")}, + } + actualJobComponents := slice.Map(rd.Spec.Jobs, func(c radixv1.RadixDeployJobComponent) deployComponentSpec { + return deployComponentSpec{Name: c.Name, Image: c.Image} + }) + s.ElementsMatch(expectedJobComponents, actualJobComponents) +} + +func (s *buildTestSuite) Test_DetectComponentsToBuild() { + appName, envName, rjName, buildBranch, jobPort := "anyapp", "dev", "anyrj", "anybranch", pointers.Ptr[int32](9999) + prepareConfigMapName := "preparecm" + rr := utils.NewRegistrationBuilder().WithName(appName).BuildRR() + rj := utils.ARadixBuildDeployJob().WithJobName(rjName).WithAppName(appName).BuildRJ() + raBuilder := utils.NewRadixApplicationBuilder(). + WithAppName(appName). + WithEnvironment(envName, buildBranch). + WithComponents( + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp").WithDockerfileName("comp.Dockerfile"), + ). + WithJobComponents( + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job").WithDockerfileName("job.Dockerfile"), + ) + defaultRa := raBuilder.WithBuildSecrets("SECRET1").BuildRA() + raWithoutSecret := raBuilder.WithBuildSecrets().BuildRA() + oldRa := defaultRa.DeepCopy() + oldRa.Spec.Components[0].Variables = radixv1.EnvVarsMap{"anyvar": "anyvalue"} + currentBuildSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: defaults.BuildSecretsName}, Data: map[string][]byte{"SECRET1": []byte("anydata")}} + oldBuildSecret := currentBuildSecret.DeepCopy() + oldBuildSecret.Data["SECRET1"] = []byte("newdata") + radixDeploymentFactory := func(annotations map[string]string, condition radixv1.RadixDeployCondition, componentBuilders []utils.DeployComponentBuilder, jobBuilders []utils.DeployJobComponentBuilder) *radixv1.RadixDeployment { + builder := utils.NewDeploymentBuilder(). + WithDeploymentName("currentrd"). + WithAppName(appName). + WithEnvironment(envName). + WithAnnotations(annotations). + WithCondition(condition). + WithActiveFrom(time.Now().Add(-1 * time.Hour)). + WithComponents(componentBuilders...). + WithJobComponents(jobBuilders...) + + if condition == radixv1.DeploymentInactive { + builder = builder.WithActiveTo(time.Now().Add(1 * time.Hour)) + } + + return builder.BuildRD() + } + piplineArgs := model.PipelineArguments{ + PipelineType: "build-deploy", + Branch: buildBranch, + JobName: rjName, + ImageTag: "imgtag", + ContainerRegistry: "registry", + Clustertype: "clustertype", + Clustername: "clustername", + } + imageNameFunc := func(s string) string { + return fmt.Sprintf("%s/%s-%s:%s", piplineArgs.ContainerRegistry, appName, s, piplineArgs.ImageTag) + } + type deployComponentSpec struct { + Name string + Image string + } + type testSpec struct { + name string + existingRd *radixv1.RadixDeployment + customRa *radixv1.RadixApplication + prepareBuildCtx *model.PrepareBuildContext + expectedJobContainers []string + expectedDeployComponents []deployComponentSpec + expectedDeployJobs []deployComponentSpec + skipEnvNamespace bool + } + tests := []testSpec{ + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, component changed, job changed - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-changed-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{"comp", "job"}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, component changed - build component", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{"comp"}, + }, + }, + }, + expectedJobContainers: []string{"build-comp"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: "job-current:anytag"}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, job changed - build job", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{"job"}, + }, + }, + }, + expectedJobContainers: []string{"build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: "comp-current:anytag"}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, component unchanged, job unchanged - no build job", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: "comp-current:anytag"}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: "job-current:anytag"}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, component unchanged, job unchanged, env namespace missing - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + skipEnvNamespace: true, + }, + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, missing prepare context for environment - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: "otherenv", + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash unchanged, component unchanged, job unchanged - no build job", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: "comp-current:anytag"}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: "job-current:anytag"}}, + }, + { + name: "radixconfig hash changed, buildsecret hash unchanged, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(oldRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash missing, buildsecret hash unchanged, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash changed, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(oldBuildSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret magic hash, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(nil)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret magic hash, no build secret, component unchanged, job unchanged - no build job", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(raWithoutSecret), kube.RadixBuildSecretHash: s.getBuildSecretHash(nil)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + customRa: raWithoutSecret, + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: "comp-current:anytag"}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: "job-current:anytag"}}, + }, + { + name: "radixconfig hash unchanged, buildsecret missing, no build secret, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(raWithoutSecret)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + customRa: raWithoutSecret, + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "radixconfig hash unchanged, buildsecret hash missing, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa)}, + radixv1.DeploymentActive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "missing current RD, component unchanged, job unchanged - build all", + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + { + name: "no current RD, component unchanged, job unchanged - build all", + existingRd: radixDeploymentFactory( + map[string]string{kube.RadixConfigHash: s.getRadixApplicationHash(defaultRa), kube.RadixBuildSecretHash: s.getBuildSecretHash(currentBuildSecret)}, + radixv1.DeploymentInactive, + []utils.DeployComponentBuilder{utils.NewDeployComponentBuilder().WithName("comp").WithImage("comp-current:anytag")}, + []utils.DeployJobComponentBuilder{utils.NewDeployJobComponentBuilder().WithName("job").WithImage("job-current:anytag")}, + ), + prepareBuildCtx: &model.PrepareBuildContext{ + EnvironmentsToBuild: []model.EnvironmentToBuild{ + { + Environment: envName, + Components: []string{}, + }, + }, + }, + expectedJobContainers: []string{"build-comp", "build-job"}, + expectedDeployComponents: []deployComponentSpec{{Name: "comp", Image: imageNameFunc("comp")}}, + expectedDeployJobs: []deployComponentSpec{{Name: "job", Image: imageNameFunc("job")}}, + }, + } + + for _, test := range tests { + + s.Run(test.name, func() { + ra := defaultRa + if test.customRa != nil { + ra = test.customRa + } + _, _ = s.kubeClient.CoreV1().Secrets(utils.GetAppNamespace(appName)).Create(context.Background(), currentBuildSecret, metav1.CreateOptions{}) + _, _ = s.radixClient.RadixV1().RadixRegistrations().Create(context.Background(), rr, metav1.CreateOptions{}) + _, _ = s.radixClient.RadixV1().RadixJobs(utils.GetAppNamespace(appName)).Create(context.Background(), rj, metav1.CreateOptions{}) + if test.existingRd != nil { + _, _ = s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).Create(context.Background(), test.existingRd, metav1.CreateOptions{}) + } + if !test.skipEnvNamespace { + _, _ = s.kubeClient.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: utils.GetEnvironmentNamespace(appName, envName)}}, metav1.CreateOptions{}) + } + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, test.prepareBuildCtx)) + pipeline := model.PipelineInfo{PipelineArguments: piplineArgs, RadixConfigMapName: prepareConfigMapName} + applyStep := steps.NewApplyConfigStep() + applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) + if len(test.expectedJobContainers) > 0 { + jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) + } + buildStep := steps.NewBuildStep(jobWaiter) + buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + + // Run pipeline steps + s.Require().NoError(applyStep.Run(&pipeline)) + s.Require().NoError(buildStep.Run(&pipeline)) + s.Require().NoError(deployStep.Run(&pipeline)) + + // Check Job containers + jobs, _ := s.kubeClient.BatchV1().Jobs(utils.GetAppNamespace(appName)).List(context.Background(), metav1.ListOptions{}) + s.Require().Equal(len(test.expectedJobContainers) > 0, len(jobs.Items) == 1) + if len(test.expectedJobContainers) > 0 { + job := jobs.Items[0] + actualJobContainers := slice.Map(job.Spec.Template.Spec.Containers, func(c corev1.Container) string { return c.Name }) + s.ElementsMatch(test.expectedJobContainers, actualJobContainers) + } + + // Check RadixDeployment component and job images + rds, _ := s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).List(context.Background(), metav1.ListOptions{LabelSelector: labels.ForPipelineJobName(rjName).String()}) + s.Require().Len(rds.Items, 1) + rd := rds.Items[0] + actualDeployComponents := slice.Map(rd.Spec.Components, func(c radixv1.RadixDeployComponent) deployComponentSpec { + return deployComponentSpec{Name: c.Name, Image: c.Image} + }) + s.ElementsMatch(test.expectedDeployComponents, actualDeployComponents) + actualJobComponents := slice.Map(rd.Spec.Jobs, func(c radixv1.RadixDeployJobComponent) deployComponentSpec { + return deployComponentSpec{Name: c.Name, Image: c.Image} + }) + s.ElementsMatch(test.expectedDeployJobs, actualJobComponents) + }) + } +} + +func (s *buildTestSuite) Test_BuildJobSpec_ImageTagNames() { + appName, envName, rjName, buildBranch, jobPort := "anyapp", "dev", "anyrj", "anybranch", pointers.Ptr[int32](9999) + prepareConfigMapName := "preparecm" + + rr := utils.NewRegistrationBuilder().WithName(appName).BuildRR() + _, _ = s.radixClient.RadixV1().RadixRegistrations().Create(context.Background(), rr, metav1.CreateOptions{}) + rj := utils.ARadixBuildDeployJob().WithJobName(rjName).WithAppName(appName).BuildRJ() + _, _ = s.radixClient.RadixV1().RadixJobs(utils.GetAppNamespace(appName)).Create(context.Background(), rj, metav1.CreateOptions{}) + ra := utils.NewRadixApplicationBuilder(). + WithAppName(appName). + WithEnvironment(envName, buildBranch). + WithComponents( + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp1").WithImage("comp1img:{imageTagName}"). + WithEnvironmentConfig(utils.NewComponentEnvironmentBuilder().WithEnvironment(envName).WithImageTagName("comp1envtag")), + utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName("comp2").WithImage("comp2img:{imageTagName}"). + WithEnvironmentConfig(utils.NewComponentEnvironmentBuilder().WithEnvironment(envName).WithImageTagName("comp2envtag")), + ). + WithJobComponents( + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job1").WithImage("job1img:{imageTagName}"). + WithEnvironmentConfig(utils.NewJobComponentEnvironmentBuilder().WithEnvironment(envName).WithImageTagName("job1envtag")), + utils.NewApplicationJobComponentBuilder().WithSchedulerPort(jobPort).WithName("job2").WithImage("job2img:{imageTagName}"). + WithEnvironmentConfig(utils.NewJobComponentEnvironmentBuilder().WithEnvironment(envName).WithImageTagName("job2envtag")), + ). + BuildRA() + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) + pipeline := model.PipelineInfo{ + PipelineArguments: model.PipelineArguments{ + PipelineType: "deploy", + ToEnvironment: envName, + JobName: rjName, + ImageTagNames: map[string]string{"comp1": "comp1customtag", "job1": "job1customtag"}, + }, + RadixConfigMapName: prepareConfigMapName, + } + + applyStep := steps.NewApplyConfigStep() + applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + + s.Require().NoError(applyStep.Run(&pipeline)) + s.Require().NoError(deployStep.Run(&pipeline)) + + // Check RadixDeployment component and job images + rds, _ := s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).List(context.Background(), metav1.ListOptions{}) + s.Require().Len(rds.Items, 1) + rd := rds.Items[0] + type deployComponentSpec struct { + Name string + Image string + } + expectedDeployComponents := []deployComponentSpec{ + {Name: "comp1", Image: "comp1img:comp1customtag"}, + {Name: "comp2", Image: "comp2img:comp2envtag"}, + } + actualDeployComponents := slice.Map(rd.Spec.Components, func(c radixv1.RadixDeployComponent) deployComponentSpec { + return deployComponentSpec{Name: c.Name, Image: c.Image} + }) + s.ElementsMatch(expectedDeployComponents, actualDeployComponents) + expectedJobComponents := []deployComponentSpec{ + {Name: "job1", Image: "job1img:job1customtag"}, + {Name: "job2", Image: "job2img:job2envtag"}, } actualJobComponents := slice.Map(rd.Spec.Jobs, func(c radixv1.RadixDeployJobComponent) deployComponentSpec { return deployComponentSpec{Name: c.Name, Image: c.Image} @@ -476,7 +1154,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_PushImage() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ Branch: "main", @@ -485,7 +1163,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_PushImage() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -516,7 +1194,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_UseCache() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ Branch: "main", @@ -525,7 +1203,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_UseCache() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -556,7 +1234,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithDockerfileName() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName).WithDockerfileName(dockerFileName)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ Branch: "main", @@ -564,7 +1242,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithDockerfileName() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -595,7 +1273,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithSourceFolder() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName).WithSourceFolder(".././path/../../subpath")). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ Branch: "main", @@ -603,7 +1281,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithSourceFolder() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -623,7 +1301,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithSourceFolder() { } func (s *buildTestSuite) Test_BuildJobSpec_WithBuildSecrets() { - appName, rjName, compName := "anyapp", "anyrj", "c1" + appName, envName, rjName, compName := "anyapp", "dev", "anyrj", "c1" prepareConfigMapName := "preparecm" rr := utils.ARadixRegistration().WithName(appName).BuildRR() _, _ = s.radixClient.RadixV1().RadixRegistrations().Create(context.Background(), rr, metav1.CreateOptions{}) @@ -632,10 +1310,10 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithBuildSecrets() { ra := utils.NewRadixApplicationBuilder(). WithAppName(appName). WithBuildSecrets("SECRET1", "SECRET2"). - WithEnvironment("dev", "main"). + WithEnvironment(envName, "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) s.Require().NoError(s.createBuildSecret(appName, map[string][]byte{"SECRET1": nil, "SECRET2": nil})) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ @@ -644,15 +1322,18 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithBuildSecrets() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() applyStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) buildStep := steps.NewBuildStep(jobWaiter) buildStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) + deployStep := steps.NewDeployStep(FakeNamespaceWatcher{}) + deployStep.Init(s.kubeClient, s.radixClient, s.kubeUtil, s.promClient, rr) s.Require().NoError(applyStep.Run(&pipeline)) s.Require().NoError(buildStep.Run(&pipeline)) + s.Require().NoError(deployStep.Run(&pipeline)) jobs, _ := s.kubeClient.BatchV1().Jobs(utils.GetAppNamespace(appName)).List(context.Background(), metav1.ListOptions{}) s.Require().Len(jobs.Items, 1) job := jobs.Items[0] @@ -667,6 +1348,11 @@ func (s *buildTestSuite) Test_BuildJobSpec_WithBuildSecrets() { {Name: defaults.BuildSecretsName, MountPath: "/build-secrets", ReadOnly: true}, } s.Subset(job.Spec.Template.Spec.Containers[0].VolumeMounts, expectedVolumeMounts) + rds, _ := s.radixClient.RadixV1().RadixDeployments(utils.GetEnvironmentNamespace(appName, envName)).List(context.Background(), metav1.ListOptions{}) + s.Require().Len(rds.Items, 1) + rd := rds.Items[0] + s.NotEmpty(rd.GetAnnotations()[kube.RadixBuildSecretHash]) + } func (s *buildTestSuite) Test_BuildJobSpec_BuildKit() { @@ -682,7 +1368,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName).WithDockerfileName(dockerFile).WithSourceFolder(sourceFolder)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ Branch: "main", @@ -697,7 +1383,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -745,7 +1431,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit_PushImage() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName).WithDockerfileName(dockerFile).WithSourceFolder(sourceFolder)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ Branch: "main", @@ -761,7 +1447,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit_PushImage() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -816,7 +1502,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit_WithBuildSecrets() { WithEnvironment("dev", "main"). WithComponent(utils.NewApplicationComponentBuilder().WithPort("any", 8080).WithName(compName).WithDockerfileName(dockerFile).WithSourceFolder(sourceFolder)). BuildRA() - s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra)) + s.Require().NoError(s.createPreparePipelineConfigMapResponse(prepareConfigMapName, appName, ra, nil)) s.Require().NoError(s.createBuildSecret(appName, map[string][]byte{"SECRET1": nil, "SECRET2": nil})) pipeline := model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ @@ -831,7 +1517,7 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit_WithBuildSecrets() { }, RadixConfigMapName: prepareConfigMapName, } - jobWaiter := pipelinewait.NewMockJobCompletionWaiter(s.ctrl) + jobWaiter := internalwait.NewMockJobCompletionWaiter(s.ctrl) jobWaiter.EXPECT().Wait(gomock.Any()).Return(nil).Times(1) applyStep := steps.NewApplyConfigStep() @@ -877,16 +1563,25 @@ func (s *buildTestSuite) Test_BuildJobSpec_BuildKit_WithBuildSecrets() { s.Equal(expectedCommand, job.Spec.Template.Spec.Containers[0].Command) } -func (s *buildTestSuite) createPreparePipelineConfigMapResponse(configMapName, appName string, ra *radixv1.RadixApplication) error { +func (s *buildTestSuite) createPreparePipelineConfigMapResponse(configMapName, appName string, ra *radixv1.RadixApplication, buildCtx *model.PrepareBuildContext) error { raBytes, err := yamlk8s.Marshal(ra) if err != nil { return err } + data := map[string]string{ + pipelineDefaults.PipelineConfigMapContent: string(raBytes), + } + + if buildCtx != nil { + buildCtxBytes, err := yaml.Marshal(buildCtx) + if err != nil { + return err + } + data[pipelineDefaults.PipelineConfigMapBuildContext] = string(buildCtxBytes) + } cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: configMapName}, - Data: map[string]string{ - pipelineDefaults.PipelineConfigMapContent: string(raBytes), - }, + Data: data, } _, err = s.kubeClient.CoreV1().ConfigMaps(utils.GetAppNamespace(appName)).Create(context.Background(), cm, metav1.CreateOptions{}) return err @@ -913,3 +1608,21 @@ func (s *buildTestSuite) createBuildSecret(appName string, data map[string][]byt _, err := s.kubeClient.CoreV1().Secrets(utils.GetAppNamespace(appName)).Create(context.Background(), secret, metav1.CreateOptions{}) return err } + +func (s *buildTestSuite) getRadixApplicationHash(ra *radixv1.RadixApplication) string { + if ra == nil { + hash, _ := hash.ToHashString(hash.SHA256, "0nXSg9l6EUepshGFmolpgV3elB0m8Mv7") + return hash + } + hash, _ := hash.ToHashString(hash.SHA256, ra.Spec) + return hash +} + +func (s *buildTestSuite) getBuildSecretHash(secret *corev1.Secret) string { + if secret == nil { + hash, _ := hash.ToHashString(hash.SHA256, "34Wd68DsJRUzrHp2f63o3U5hUD6zl8Tj") + return hash + } + hash, _ := hash.ToHashString(hash.SHA256, secret.Data) + return hash +} diff --git a/pipeline-runner/steps/deploy.go b/pipeline-runner/steps/deploy.go index 58ec63920..32e0bd887 100644 --- a/pipeline-runner/steps/deploy.go +++ b/pipeline-runner/steps/deploy.go @@ -56,16 +56,12 @@ func (cli *DeployStepImplementation) deploy(pipelineInfo *model.PipelineInfo) er appName := cli.GetAppName() log.Infof("Deploying app %s", appName) - if !pipelineInfo.BranchIsMapped { + if len(pipelineInfo.TargetEnvironments) == 0 { log.Infof("skip deploy step as branch %s is not mapped to any environment", pipelineInfo.PipelineArguments.Branch) return nil } - for env, shouldDeploy := range pipelineInfo.TargetEnvironments { - if !shouldDeploy { - continue - } - + for _, env := range pipelineInfo.TargetEnvironments { err := cli.deployToEnv(appName, env, pipelineInfo) if err != nil { return err @@ -77,7 +73,6 @@ func (cli *DeployStepImplementation) deploy(pipelineInfo *model.PipelineInfo) er func (cli *DeployStepImplementation) deployToEnv(appName, env string, pipelineInfo *model.PipelineInfo) error { defaultEnvVars, err := getDefaultEnvVars(pipelineInfo) - if err != nil { return fmt.Errorf("failed to retrieve default env vars for RadixDeployment in app %s. %v", appName, err) } @@ -85,14 +80,28 @@ func (cli *DeployStepImplementation) deployToEnv(appName, env string, pipelineIn if commitID, ok := defaultEnvVars[defaults.RadixCommitHashEnvironmentVariable]; !ok || len(commitID) == 0 { defaultEnvVars[defaults.RadixCommitHashEnvironmentVariable] = pipelineInfo.PipelineArguments.CommitID // Commit ID specified by job arguments } + + radixApplicationHash, err := createRadixApplicationHash(pipelineInfo.RadixApplication) + if err != nil { + return err + } + + buildSecretHash, err := createBuildSecretHash(pipelineInfo.BuildSecret) + if err != nil { + return err + } + radixDeployment, err := deployment.ConstructForTargetEnvironment( pipelineInfo.RadixApplication, pipelineInfo.PipelineArguments.JobName, pipelineInfo.PipelineArguments.ImageTag, pipelineInfo.PipelineArguments.Branch, - pipelineInfo.ComponentImages, + pipelineInfo.DeployEnvironmentComponentImages[env], env, - defaultEnvVars) + defaultEnvVars, + radixApplicationHash, + buildSecretHash, + ) if err != nil { return fmt.Errorf("failed to create radix deployments objects for app %s. %v", appName, err) diff --git a/pipeline-runner/steps/deploy_test.go b/pipeline-runner/steps/deploy_test.go index 1cd6f29e6..40ad8193e 100644 --- a/pipeline-runner/steps/deploy_test.go +++ b/pipeline-runner/steps/deploy_test.go @@ -75,8 +75,8 @@ func TestDeploy_BranchIsNotMapped_ShouldSkip(t *testing.T) { cli := steps.NewDeployStep(FakeNamespaceWatcher{}) cli.Init(kubeclient, radixclient, kubeUtil, &monitoring.Clientset{}, rr) - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) - branchIsMapped, targetEnvs := applicationConfig.IsThereAnythingToDeploy(anyNoMappedBranch) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + targetEnvs := applicationConfig.GetTargetEnvironments(anyNoMappedBranch) pipelineInfo := &model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ @@ -86,7 +86,6 @@ func TestDeploy_BranchIsNotMapped_ShouldSkip(t *testing.T) { CommitID: anyCommitID, }, TargetEnvironments: targetEnvs, - BranchIsMapped: branchIsMapped, } err := cli.Run(pipelineInfo) @@ -181,8 +180,8 @@ func TestDeploy_PromotionSetup_ShouldCreateNamespacesForAllBranchesIfNotExists(t cli := steps.NewDeployStep(FakeNamespaceWatcher{}) cli.Init(kubeclient, radixclient, kubeUtil, &monitoring.Clientset{}, rr) - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) - branchIsMapped, targetEnvs := applicationConfig.IsThereAnythingToDeploy("master") + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + targetEnvs := applicationConfig.GetTargetEnvironments("master") pipelineInfo := &model.PipelineInfo{ PipelineArguments: model.PipelineArguments{ @@ -191,7 +190,6 @@ func TestDeploy_PromotionSetup_ShouldCreateNamespacesForAllBranchesIfNotExists(t Branch: "master", CommitID: anyCommitID, }, - BranchIsMapped: branchIsMapped, TargetEnvironments: targetEnvs, GitCommitHash: anyCommitID, GitTags: anyGitTags, @@ -297,7 +295,7 @@ func TestDeploy_SetCommitID_whenSet(t *testing.T) { cli := steps.NewDeployStep(FakeNamespaceWatcher{}) cli.Init(kubeclient, radixclient, kubeUtil, &monitoring.Clientset{}, rr) - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) const commitID = "222ca8595c5283a9d0f17a623b9255a0d9866a2e" @@ -308,8 +306,7 @@ func TestDeploy_SetCommitID_whenSet(t *testing.T) { Branch: "master", CommitID: anyCommitID, }, - BranchIsMapped: true, - TargetEnvironments: map[string]bool{"master": true}, + TargetEnvironments: []string{"master"}, GitCommitHash: commitID, GitTags: "", } diff --git a/pipeline-runner/steps/hash.go b/pipeline-runner/steps/hash.go new file mode 100644 index 000000000..2e69c1b9c --- /dev/null +++ b/pipeline-runner/steps/hash.go @@ -0,0 +1,43 @@ +package steps + +import ( + "github.com/equinor/radix-operator/pipeline-runner/internal/hash" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" + corev1 "k8s.io/api/core/v1" +) + +// Constants used to generate hash for RadixApplication and BuildSecret if they are nil. Do not change. +const ( + magicValueForNilRadixApplication = "0nXSg9l6EUepshGFmolpgV3elB0m8Mv7" + magicValueForNilBuildSecretData = "34Wd68DsJRUzrHp2f63o3U5hUD6zl8Tj" +) + +func createRadixApplicationHash(ra *radixv1.RadixApplication) (string, error) { + return hash.ToHashString(hash.SHA256, getRadixApplicationOrMagicValue(ra)) +} + +func compareRadixApplicationHash(targetHash string, ra *radixv1.RadixApplication) (bool, error) { + return hash.CompareWithHashString(getRadixApplicationOrMagicValue(ra), targetHash) +} + +func getRadixApplicationOrMagicValue(ra *radixv1.RadixApplication) any { + if ra == nil { + return magicValueForNilRadixApplication + } + return ra.Spec +} + +func createBuildSecretHash(secret *corev1.Secret) (string, error) { + return hash.ToHashString(hash.SHA256, getBuildSecretOrMagicValue(secret)) +} + +func compareBuildSecretHash(targetHash string, secret *corev1.Secret) (bool, error) { + return hash.CompareWithHashString(getBuildSecretOrMagicValue(secret), targetHash) +} + +func getBuildSecretOrMagicValue(secret *corev1.Secret) any { + if secret == nil || len(secret.Data) == 0 { + return magicValueForNilBuildSecretData + } + return secret.Data +} diff --git a/pipeline-runner/steps/prepare_pipelines.go b/pipeline-runner/steps/prepare_pipelines.go index 1336472a6..9e681f71e 100644 --- a/pipeline-runner/steps/prepare_pipelines.go +++ b/pipeline-runner/steps/prepare_pipelines.go @@ -4,10 +4,10 @@ import ( "context" "fmt" + internaltekton "github.com/equinor/radix-operator/pipeline-runner/internal/tekton" + internalwait "github.com/equinor/radix-operator/pipeline-runner/internal/wait" "github.com/equinor/radix-operator/pipeline-runner/model" pipelineDefaults "github.com/equinor/radix-operator/pipeline-runner/model/defaults" - pipelineinternal "github.com/equinor/radix-operator/pipeline-runner/utils" - pipelinewait "github.com/equinor/radix-operator/pipeline-runner/wait" "github.com/equinor/radix-operator/pkg/apis/applicationconfig" "github.com/equinor/radix-operator/pkg/apis/defaults" jobUtil "github.com/equinor/radix-operator/pkg/apis/job" @@ -29,12 +29,12 @@ import ( type PreparePipelinesStepImplementation struct { stepType pipeline.StepType model.DefaultStepImplementation - jobWaiter pipelinewait.JobCompletionWaiter + jobWaiter internalwait.JobCompletionWaiter } // NewPreparePipelinesStep Constructor. // jobWaiter is optional and will be set by Init(...) function if nil. -func NewPreparePipelinesStep(jobWaiter pipelinewait.JobCompletionWaiter) model.Step { +func NewPreparePipelinesStep(jobWaiter internalwait.JobCompletionWaiter) model.Step { return &PreparePipelinesStepImplementation{ stepType: pipeline.PreparePipelinesStep, jobWaiter: jobWaiter, @@ -44,7 +44,7 @@ func NewPreparePipelinesStep(jobWaiter pipelinewait.JobCompletionWaiter) model.S func (step *PreparePipelinesStepImplementation) Init(kubeclient kubernetes.Interface, radixclient radixclient.Interface, kubeutil *kube.Kube, prometheusOperatorClient monitoring.Interface, rr *radixv1.RadixRegistration) { step.DefaultStepImplementation.Init(kubeclient, radixclient, kubeutil, prometheusOperatorClient, rr) if step.jobWaiter == nil { - step.jobWaiter = pipelinewait.NewJobCompletionWaiter(kubeclient) + step.jobWaiter = internalwait.NewJobCompletionWaiter(kubeclient) } } @@ -71,7 +71,7 @@ func (cli *PreparePipelinesStepImplementation) Run(pipelineInfo *model.PipelineI namespace := utils.GetAppNamespace(appName) log.Infof("Prepare pipelines app %s for branch %s and commit %s", appName, branch, commitID) - if radixv1.RadixPipelineType(pipelineInfo.PipelineArguments.PipelineType) == radixv1.Promote { + if pipelineInfo.IsPipelineType(radixv1.Promote) { sourceDeploymentGitCommitHash, sourceDeploymentGitBranch, err := cli.getSourceDeploymentGitInfo(appName, pipelineInfo.PipelineArguments.FromEnvironment, pipelineInfo.PipelineArguments.DeploymentName) if err != nil { return err @@ -179,12 +179,12 @@ func (cli *PreparePipelinesStepImplementation) getPreparePipelinesJobConfig(pipe sshURL := registration.Spec.CloneURL initContainers := cli.getInitContainerCloningRepo(pipelineInfo, configBranch, sshURL) - return pipelineinternal.CreateActionPipelineJob(defaults.RadixPipelineJobPreparePipelinesContainerName, action, pipelineInfo, appName, initContainers, &envVars) + return internaltekton.CreateActionPipelineJob(defaults.RadixPipelineJobPreparePipelinesContainerName, action, pipelineInfo, appName, initContainers, &envVars) } func getWebhookCommitID(pipelineInfo *model.PipelineInfo) string { - if pipelineInfo.PipelineArguments.PipelineType == string(radixv1.BuildDeploy) { + if pipelineInfo.IsPipelineType(radixv1.BuildDeploy) { return pipelineInfo.PipelineArguments.CommitID } return "" diff --git a/pipeline-runner/steps/promotion.go b/pipeline-runner/steps/promotion.go index 742285fdf..1d9adc68c 100644 --- a/pipeline-runner/steps/promotion.go +++ b/pipeline-runner/steps/promotion.go @@ -124,7 +124,7 @@ func (cli *PromoteStepImplementation) Run(pipelineInfo *model.PipelineInfo) erro radixDeployment.Labels[kube.RadixJobNameLabel] = pipelineInfo.PipelineArguments.JobName radixDeployment.Spec.Environment = pipelineInfo.PipelineArguments.ToEnvironment - err = mergeWithRadixApplication(radixApplication, radixDeployment, pipelineInfo.PipelineArguments.ToEnvironment, pipelineInfo.ComponentImages) + err = mergeWithRadixApplication(radixApplication, radixDeployment, pipelineInfo.PipelineArguments.ToEnvironment, pipelineInfo.DeployEnvironmentComponentImages[pipelineInfo.PipelineArguments.ToEnvironment]) if err != nil { return err } @@ -165,7 +165,7 @@ func areArgumentsValid(arguments model.PipelineArguments) error { return nil } -func mergeWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment *v1.RadixDeployment, environment string, componentImages map[string]pipeline.ComponentImage) error { +func mergeWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment *v1.RadixDeployment, environment string, componentImages pipeline.DeployComponentImages) error { defaultEnvVars := getDefaultEnvVarsFromRadixDeployment(radixDeployment) if err := mergeComponentsWithRadixApplication(radixConfig, radixDeployment, environment, defaultEnvVars, componentImages); err != nil { return err @@ -178,7 +178,7 @@ func mergeWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment return nil } -func mergeJobComponentsWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment *v1.RadixDeployment, environment string, defaultEnvVars v1.EnvVarsMap, componentImages map[string]pipeline.ComponentImage) error { +func mergeJobComponentsWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment *v1.RadixDeployment, environment string, defaultEnvVars v1.EnvVarsMap, componentImages pipeline.DeployComponentImages) error { newEnvJobs, err := deployment. NewJobComponentsBuilder(radixConfig, environment, componentImages, defaultEnvVars). JobComponents() @@ -204,7 +204,7 @@ func mergeJobComponentsWithRadixApplication(radixConfig *v1.RadixApplication, ra return nil } -func mergeComponentsWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment *v1.RadixDeployment, environment string, defaultEnvVars v1.EnvVarsMap, componentImages map[string]pipeline.ComponentImage) error { +func mergeComponentsWithRadixApplication(radixConfig *v1.RadixApplication, radixDeployment *v1.RadixDeployment, environment string, defaultEnvVars v1.EnvVarsMap, componentImages pipeline.DeployComponentImages) error { newEnvComponents, err := deployment.GetRadixComponentsForEnv(radixConfig, environment, componentImages, defaultEnvVars) if err != nil { return err diff --git a/pipeline-runner/steps/promotion_test.go b/pipeline-runner/steps/promotion_test.go index 743ce5159..1dc8b865a 100644 --- a/pipeline-runner/steps/promotion_test.go +++ b/pipeline-runner/steps/promotion_test.go @@ -274,7 +274,7 @@ func TestPromote_PromoteToOtherEnvironment_NewStateIsExpected(t *testing.T) { }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) gitCommitHash := pipelineInfo.GitCommitHash gitTags := pipelineInfo.GitTags pipelineInfo.SetApplicationConfig(applicationConfig) @@ -399,7 +399,7 @@ func TestPromote_PromoteToOtherEnvironment_Resources_NoOverride(t *testing.T) { }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) gitCommitHash := pipelineInfo.GitCommitHash gitTags := pipelineInfo.GitTags pipelineInfo.SetApplicationConfig(applicationConfig) @@ -493,7 +493,7 @@ func TestPromote_PromoteToOtherEnvironment_Authentication(t *testing.T) { }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) gitCommitHash := pipelineInfo.GitCommitHash gitTags := pipelineInfo.GitTags pipelineInfo.SetApplicationConfig(applicationConfig) @@ -609,7 +609,7 @@ func TestPromote_PromoteToOtherEnvironment_Resources_WithOverride(t *testing.T) }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) gitCommitHash := pipelineInfo.GitCommitHash gitTags := pipelineInfo.GitTags pipelineInfo.SetApplicationConfig(applicationConfig) @@ -670,7 +670,7 @@ func TestPromote_PromoteToSameEnvironment_NewStateIsExpected(t *testing.T) { }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) gitCommitHash := pipelineInfo.GitCommitHash gitTags := pipelineInfo.GitTags pipelineInfo.SetApplicationConfig(applicationConfig) @@ -788,7 +788,7 @@ func TestPromote_PromoteToOtherEnvironment_Identity(t *testing.T) { }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) pipelineInfo.SetApplicationConfig(applicationConfig) err = cli.Run(pipelineInfo) require.NoError(t, err) @@ -805,11 +805,11 @@ func TestPromote_PromoteToOtherEnvironment_Identity(t *testing.T) { func TestPromote_AnnotatedBySourceDeploymentAttributes(t *testing.T) { srcDeploymentName := "deployment-1" anyImageTag := "abcdef" - anyBuildDeployJobName := "any-build-deploy-job" anyPromoteJobName := "any-promote-job" dstEnv := "test" srcEnv := "dev" srcDeploymentCommitID := "222ca8595c5283a9d0f17a623b9255a0d9866a2e" + srcRadixConfigHash := "sha256-a5d1565b32252be05910e459eb7551fd0fd6e0d513f7728c54ca5507c9b11387" // Setup kubeclient, kubeUtil, radixclient, commonTestUtils := setupTest(t) @@ -827,8 +827,9 @@ func TestPromote_AnnotatedBySourceDeploymentAttributes(t *testing.T) { WithDeploymentName(srcDeploymentName). WithEnvironment(srcEnv). WithImageTag(anyImageTag). - WithLabel(kube.RadixJobNameLabel, anyBuildDeployJobName). - WithLabel(kube.RadixCommitLabel, srcDeploymentCommitID)) + WithLabel(kube.RadixCommitLabel, srcDeploymentCommitID). + WithAnnotations(map[string]string{kube.RadixConfigHash: srcRadixConfigHash})) + require.NoError(t, err) rr, _ := radixclient.RadixV1().RadixRegistrations().Get(context.TODO(), anyAppName, metav1.GetOptions{}) @@ -848,7 +849,7 @@ func TestPromote_AnnotatedBySourceDeploymentAttributes(t *testing.T) { }, } - applicationConfig, _ := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) + applicationConfig := application.NewApplicationConfig(kubeclient, kubeUtil, radixclient, rr, ra) gitCommitHash := pipelineInfo.GitCommitHash gitTags := pipelineInfo.GitTags pipelineInfo.SetApplicationConfig(applicationConfig) @@ -862,5 +863,6 @@ func TestPromote_AnnotatedBySourceDeploymentAttributes(t *testing.T) { promotedRD := rds.Items[0] assert.Equal(t, srcEnv, promotedRD.GetAnnotations()[kube.RadixDeploymentPromotedFromEnvironmentAnnotation]) assert.Equal(t, srcDeploymentName, promotedRD.GetAnnotations()[kube.RadixDeploymentPromotedFromDeploymentAnnotation]) + assert.Equal(t, srcRadixConfigHash, promotedRD.GetAnnotations()[kube.RadixConfigHash]) assert.Equal(t, srcDeploymentCommitID, promotedRD.GetLabels()[kube.RadixCommitLabel]) } diff --git a/pipeline-runner/steps/run_pipelines.go b/pipeline-runner/steps/run_pipelines.go index 596dadeb8..7c17e8d1a 100644 --- a/pipeline-runner/steps/run_pipelines.go +++ b/pipeline-runner/steps/run_pipelines.go @@ -4,10 +4,10 @@ import ( "context" "fmt" + internaltekton "github.com/equinor/radix-operator/pipeline-runner/internal/tekton" + internalwait "github.com/equinor/radix-operator/pipeline-runner/internal/wait" "github.com/equinor/radix-operator/pipeline-runner/model" pipelineDefaults "github.com/equinor/radix-operator/pipeline-runner/model/defaults" - pipelineinternal "github.com/equinor/radix-operator/pipeline-runner/utils" - pipelinewait "github.com/equinor/radix-operator/pipeline-runner/wait" "github.com/equinor/radix-operator/pkg/apis/defaults" jobUtil "github.com/equinor/radix-operator/pkg/apis/job" "github.com/equinor/radix-operator/pkg/apis/kube" @@ -27,12 +27,12 @@ import ( type RunPipelinesStepImplementation struct { stepType pipeline.StepType model.DefaultStepImplementation - jobWaiter pipelinewait.JobCompletionWaiter + jobWaiter internalwait.JobCompletionWaiter } // NewRunPipelinesStep Constructor. // jobWaiter is optional and will be set by Init(...) function if nil. -func NewRunPipelinesStep(jobWaiter pipelinewait.JobCompletionWaiter) model.Step { +func NewRunPipelinesStep(jobWaiter internalwait.JobCompletionWaiter) model.Step { return &RunPipelinesStepImplementation{ stepType: pipeline.RunPipelinesStep, jobWaiter: jobWaiter, @@ -42,7 +42,7 @@ func NewRunPipelinesStep(jobWaiter pipelinewait.JobCompletionWaiter) model.Step func (step *RunPipelinesStepImplementation) Init(kubeclient kubernetes.Interface, radixclient radixclient.Interface, kubeutil *kube.Kube, prometheusOperatorClient monitoring.Interface, rr *v1.RadixRegistration) { step.DefaultStepImplementation.Init(kubeclient, radixclient, kubeutil, prometheusOperatorClient, rr) if step.jobWaiter == nil { - step.jobWaiter = pipelinewait.NewJobCompletionWaiter(kubeclient) + step.jobWaiter = internalwait.NewJobCompletionWaiter(kubeclient) } } @@ -144,5 +144,5 @@ func (cli *RunPipelinesStepImplementation) getRunTektonPipelinesJobConfig(pipeli Value: pipelineInfo.PipelineArguments.LogLevel, }, } - return pipelineinternal.CreateActionPipelineJob(defaults.RadixPipelineJobRunPipelinesContainerName, action, pipelineInfo, appName, nil, &envVars) + return internaltekton.CreateActionPipelineJob(defaults.RadixPipelineJobRunPipelinesContainerName, action, pipelineInfo, appName, nil, &envVars) } diff --git a/pkg/apis/applicationconfig/applicationconfig.go b/pkg/apis/applicationconfig/applicationconfig.go index 560493265..4404b42fa 100644 --- a/pkg/apis/applicationconfig/applicationconfig.go +++ b/pkg/apis/applicationconfig/applicationconfig.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/equinor/radix-operator/pkg/apis/defaults" "reflect" "strings" + "github.com/equinor/radix-operator/pkg/apis/defaults" + "github.com/equinor/radix-operator/pkg/apis/utils/branch" "github.com/equinor/radix-operator/pkg/apis/kube" @@ -40,13 +41,13 @@ func NewApplicationConfig( kubeutil *kube.Kube, radixclient radixclient.Interface, registration *v1.RadixRegistration, - config *v1.RadixApplication) (*ApplicationConfig, error) { + config *v1.RadixApplication) *ApplicationConfig { return &ApplicationConfig{ kubeclient, radixclient, kubeutil, registration, - config}, nil + config} } // GetRadixApplicationConfig returns the provided config @@ -104,18 +105,15 @@ func IsConfigBranch(branch string, rr *v1.RadixRegistration) bool { return strings.EqualFold(branch, GetConfigBranch(rr)) } -// IsThereAnythingToDeploy Checks if given branch requires deployment to environments -func (app *ApplicationConfig) IsThereAnythingToDeploy(branch string) (bool, map[string]bool) { - return IsThereAnythingToDeployForRadixApplication(branch, app.config) -} - -// IsThereAnythingToDeployForRadixApplication Checks if given branch requires deployment to environments -func IsThereAnythingToDeployForRadixApplication(branch string, ra *v1.RadixApplication) (bool, map[string]bool) { - targetEnvs := getTargetEnvironmentsAsMap(branch, ra) - if isTargetEnvsEmpty(targetEnvs) { - return false, targetEnvs +// GetTargetEnvironments Checks if given branch requires deployment to environments +func (app *ApplicationConfig) GetTargetEnvironments(branchToBuild string) []string { + var targetEnvs []string + for _, env := range app.config.Spec.Environments { + if env.Build.From != "" && branch.MatchesPattern(env.Build.From, branchToBuild) { + targetEnvs = append(targetEnvs, env.Name) + } } - return true, targetEnvs + return targetEnvs } // ApplyConfigToApplicationNamespace Will apply the config to app namespace so that the operator can act on it @@ -211,35 +209,6 @@ func (app *ApplicationConfig) createEnvironments() error { return nil } -func getTargetEnvironmentsAsMap(branchToBuild string, radixApplication *v1.RadixApplication) map[string]bool { - targetEnvs := make(map[string]bool) - for _, env := range radixApplication.Spec.Environments { - if env.Build.From != "" && branch.MatchesPattern(env.Build.From, branchToBuild) { - // Deploy environment - targetEnvs[env.Name] = true - } else { - // Only create namespace for environment - targetEnvs[env.Name] = false - } - } - return targetEnvs -} - -func isTargetEnvsEmpty(targetEnvs map[string]bool) bool { - if len(targetEnvs) == 0 { - return true - } - - // Check if all values are false - falseCount := 0 - for _, value := range targetEnvs { - if !value { - falseCount++ - } - } - return falseCount == len(targetEnvs) -} - // applyEnvironment creates an environment or applies changes if it exists func (app *ApplicationConfig) applyEnvironment(newRe *v1.RadixEnvironment) error { logger := log.WithFields(log.Fields{"environment": newRe.ObjectMeta.Name}) diff --git a/pkg/apis/applicationconfig/applicationconfig_test.go b/pkg/apis/applicationconfig/applicationconfig_test.go index c59f6db67..2e0966703 100644 --- a/pkg/apis/applicationconfig/applicationconfig_test.go +++ b/pkg/apis/applicationconfig/applicationconfig_test.go @@ -45,7 +45,7 @@ func setupTest() (*test.Utils, kubernetes.Interface, *kube.Kube, radixclient.Int func getApplication(ra *radixv1.RadixApplication) *ApplicationConfig { // The other arguments are not relevant for this test - application, _ := NewApplicationConfig(nil, nil, nil, nil, ra) + application := NewApplicationConfig(nil, nil, nil, nil, ra) return application } @@ -54,7 +54,7 @@ func Test_Create_Radix_Environments(t *testing.T) { radixRegistration, _ := utils.GetRadixRegistrationFromFile(sampleRegistration) radixApp, _ := utils.GetRadixApplicationFromFile(sampleApp) - app, _ := NewApplicationConfig(client, kubeUtil, radixclient, radixRegistration, radixApp) + app := NewApplicationConfig(client, kubeUtil, radixclient, radixRegistration, radixApp) label := fmt.Sprintf("%s=%s", kube.RadixAppLabel, radixRegistration.Name) t.Run("It can create environments", func(t *testing.T) { @@ -115,7 +115,7 @@ func Test_Reconciles_Radix_Environments(t *testing.T) { WithEnvironment("prod", "master"). BuildRA() - app, _ := NewApplicationConfig(client, kubeUtil, radixclient, rr, ra) + app := NewApplicationConfig(client, kubeUtil, radixclient, rr, ra) label := fmt.Sprintf("%s=%s", kube.RadixAppLabel, rr.Name) // Test @@ -137,12 +137,8 @@ func TestIsThereAnythingToDeploy_multipleEnvsToOneBranch_ListsBoth(t *testing.T) BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.True(t, isThereAnythingToDeploy) - assert.Equal(t, 2, len(targetEnvs)) - assert.Equal(t, targetEnvs["prod"], true) - assert.Equal(t, targetEnvs["qa"], true) + targetEnvs := application.GetTargetEnvironments(branch) + assert.ElementsMatch(t, []string{"prod", "qa"}, targetEnvs) } func TestIsThereAnythingToDeploy_multipleEnvsToOneBranchOtherBranchIsChanged_ListsBothButNoneIsBuilding(t *testing.T) { @@ -154,12 +150,8 @@ func TestIsThereAnythingToDeploy_multipleEnvsToOneBranchOtherBranchIsChanged_Lis BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.False(t, isThereAnythingToDeploy) - assert.Equal(t, 2, len(targetEnvs)) - assert.Equal(t, targetEnvs["prod"], false) - assert.Equal(t, targetEnvs["qa"], false) + targetEnvs := application.GetTargetEnvironments(branch) + assert.Equal(t, 0, len(targetEnvs)) } func TestIsThereAnythingToDeploy_oneEnvToOneBranch_ListsBothButOnlyOneShouldBeBuilt(t *testing.T) { @@ -171,12 +163,8 @@ func TestIsThereAnythingToDeploy_oneEnvToOneBranch_ListsBothButOnlyOneShouldBeBu BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.True(t, isThereAnythingToDeploy) - assert.Equal(t, 2, len(targetEnvs)) - assert.Equal(t, targetEnvs["prod"], false) - assert.Equal(t, targetEnvs["qa"], true) + targetEnvs := application.GetTargetEnvironments(branch) + assert.ElementsMatch(t, []string{"qa"}, targetEnvs) } func TestIsThereAnythingToDeploy_twoEnvNoBranch(t *testing.T) { @@ -188,12 +176,8 @@ func TestIsThereAnythingToDeploy_twoEnvNoBranch(t *testing.T) { BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.False(t, isThereAnythingToDeploy) - assert.Equal(t, 2, len(targetEnvs)) - assert.Equal(t, false, targetEnvs["qa"]) - assert.Equal(t, false, targetEnvs["prod"]) + targetEnvs := application.GetTargetEnvironments(branch) + assert.Equal(t, 0, len(targetEnvs)) } func TestIsThereAnythingToDeploy_NoEnv(t *testing.T) { @@ -203,9 +187,7 @@ func TestIsThereAnythingToDeploy_NoEnv(t *testing.T) { BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.False(t, isThereAnythingToDeploy) + targetEnvs := application.GetTargetEnvironments(branch) assert.Equal(t, 0, len(targetEnvs)) } @@ -218,12 +200,8 @@ func TestIsThereAnythingToDeploy_promotionScheme_ListsBothButOnlyOneShouldBeBuil BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.True(t, isThereAnythingToDeploy) - assert.Equal(t, 2, len(targetEnvs)) - assert.Equal(t, targetEnvs["prod"], false) - assert.Equal(t, targetEnvs["qa"], true) + targetEnvs := application.GetTargetEnvironments(branch) + assert.ElementsMatch(t, []string{"qa"}, targetEnvs) } func TestIsThereAnythingToDeploy_wildcardMatch_ListsBothButOnlyOneShouldBeBuilt(t *testing.T) { @@ -235,41 +213,8 @@ func TestIsThereAnythingToDeploy_wildcardMatch_ListsBothButOnlyOneShouldBeBuilt( BuildRA() application := getApplication(ra) - isThereAnythingToDeploy, targetEnvs := application.IsThereAnythingToDeploy(branch) - - assert.True(t, isThereAnythingToDeploy) - assert.Equal(t, 2, len(targetEnvs)) - assert.Equal(t, targetEnvs["prod"], false) - assert.Equal(t, targetEnvs["feature"], true) -} - -func TestIsTargetEnvsEmpty_noEntry(t *testing.T) { - targetEnvs := map[string]bool{} - assert.Equal(t, true, isTargetEnvsEmpty(targetEnvs)) -} - -func TestIsTargetEnvsEmpty_twoEntriesWithBranchMapping(t *testing.T) { - targetEnvs := map[string]bool{ - "qa": true, - "prod": true, - } - assert.Equal(t, false, isTargetEnvsEmpty(targetEnvs)) -} - -func TestIsTargetEnvsEmpty_twoEntriesWithNoMapping(t *testing.T) { - targetEnvs := map[string]bool{ - "qa": false, - "prod": false, - } - assert.Equal(t, true, isTargetEnvsEmpty(targetEnvs)) -} - -func TestIsTargetEnvsEmpty_twoEntriesWithOneMapping(t *testing.T) { - targetEnvs := map[string]bool{ - "qa": true, - "prod": false, - } - assert.Equal(t, false, isTargetEnvsEmpty(targetEnvs)) + targetEnvs := application.GetTargetEnvironments(branch) + assert.ElementsMatch(t, []string{"feature"}, targetEnvs) } func Test_WithBuildSecretsSet_SecretsCorrectlyAdded(t *testing.T) { @@ -406,7 +351,7 @@ func Test_AppReaderPrivateImageHubRoleAndRoleBindingExists(t *testing.T) { assert.True(t, roleBindingByNameExists("radix-private-image-hubs-reader", rolebindings)) } func Test_WithPrivateImageHubSet_SecretsCorrectly_Added(t *testing.T) { - client, _, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abbc", Email: "radix@equinor.com", @@ -421,7 +366,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_Added(t *testing.T) { } func Test_WithPrivateImageHubSet_SecretsCorrectly_SetPassword(t *testing.T) { - client, appConfig, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, appConfig := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abbc", Email: "radix@equinor.com", @@ -449,7 +394,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_UpdatedNewAdded(t *testing.T) }, }) - client, _, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abbc", Email: "radix@equinor.com", @@ -475,7 +420,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_UpdateUsername(t *testing.T) { }, }) - client, _, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abb2", Email: "radix@equinor.com", @@ -497,7 +442,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_UpdateServerName(t *testing.T) }, }) - client, _, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme1.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abbc", Email: "radix@equinor.com", @@ -511,7 +456,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_UpdateServerName(t *testing.T) } func Test_WithPrivateImageHubSet_SecretsCorrectly_Delete(t *testing.T) { - client, _, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abbc", Email: "radix@equinor.com", @@ -527,7 +472,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_Delete(t *testing.T) { "{\"auths\":{\"privaterepodeleteme.azurecr.io\":{\"username\":\"814607e6-3d71-44a7-8476-50e8b281abbc\",\"password\":\"\",\"email\":\"radix@equinor.com\",\"auth\":\"ODE0NjA3ZTYtM2Q3MS00NGE3LTg0NzYtNTBlOGIyODFhYmJjOg==\"},\"privaterepodeleteme2.azurecr.io\":{\"username\":\"814607e6-3d71-44a7-8476-50e8b281abbc\",\"password\":\"\",\"email\":\"radix@equinor.com\",\"auth\":\"ODE0NjA3ZTYtM2Q3MS00NGE3LTg0NzYtNTBlOGIyODFhYmJjOg==\"}}}", string(secret.Data[corev1.DockerConfigJsonKey])) - client, _, _ = applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ + client, _ = applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{ "privaterepodeleteme2.azurecr.io": &radixv1.RadixPrivateImageHubCredential{ Username: "814607e6-3d71-44a7-8476-50e8b281abbc", Email: "radix@equinor.com", @@ -541,7 +486,7 @@ func Test_WithPrivateImageHubSet_SecretsCorrectly_Delete(t *testing.T) { } func Test_WithPrivateImageHubSet_SecretsCorrectly_NoImageHubs(t *testing.T) { - client, appConfig, _ := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{}) + client, appConfig := applyRadixAppWithPrivateImageHub(radixv1.PrivateImageHubEntries{}) pendingSecrets, _ := appConfig.GetPendingPrivateImageHubSecrets() secret, _ := client.CoreV1().Secrets("any-app-app").Get(context.TODO(), defaults.PrivateImageHubSecretName, metav1.GetOptions{}) @@ -672,7 +617,7 @@ func rrAsOwnerReference(rr *radixv1.RadixRegistration) []metav1.OwnerReference { } } -func applyRadixAppWithPrivateImageHub(privateImageHubs radixv1.PrivateImageHubEntries) (kubernetes.Interface, *ApplicationConfig, error) { +func applyRadixAppWithPrivateImageHub(privateImageHubs radixv1.PrivateImageHubEntries) (kubernetes.Interface, *ApplicationConfig) { tu, client, kubeUtil, radixclient := setupTest() appBuilder := utils.ARadixApplication(). WithAppName("any-app"). @@ -682,11 +627,11 @@ func applyRadixAppWithPrivateImageHub(privateImageHubs radixv1.PrivateImageHubEn } applyApplicationWithSync(tu, client, kubeUtil, radixclient, appBuilder) - appConfig, err := getAppConfig(client, kubeUtil, radixclient, appBuilder) - return client, appConfig, err + appConfig := getAppConfig(client, kubeUtil, radixclient, appBuilder) + return client, appConfig } -func getAppConfig(client kubernetes.Interface, kubeUtil *kube.Kube, radixclient radixclient.Interface, applicationBuilder utils.ApplicationBuilder) (*ApplicationConfig, error) { +func getAppConfig(client kubernetes.Interface, kubeUtil *kube.Kube, radixclient radixclient.Interface, applicationBuilder utils.ApplicationBuilder) *ApplicationConfig { ra := applicationBuilder.BuildRA() radixRegistration, _ := radixclient.RadixV1().RadixRegistrations().Get(context.TODO(), ra.Name, metav1.GetOptions{}) @@ -706,10 +651,7 @@ func applyApplicationWithSync(tu *test.Utils, client kubernetes.Interface, kubeU return err } - applicationconfig, err := NewApplicationConfig(client, kubeUtil, radixclient, radixRegistration, ra) - if err != nil { - return err - } + applicationconfig := NewApplicationConfig(client, kubeUtil, radixclient, radixRegistration, ra) err = applicationconfig.OnSync() if err != nil { diff --git a/pkg/apis/deployment/deployment.go b/pkg/apis/deployment/deployment.go index 0f7312628..26bf083e9 100644 --- a/pkg/apis/deployment/deployment.go +++ b/pkg/apis/deployment/deployment.go @@ -94,7 +94,7 @@ func GetDeploymentJobComponent(rd *v1.RadixDeployment, name string) (int, *v1.Ra } // ConstructForTargetEnvironment Will build a deployment for target environment -func ConstructForTargetEnvironment(config *v1.RadixApplication, jobName string, imageTag string, branch string, componentImages map[string]pipeline.ComponentImage, env string, defaultEnvVars v1.EnvVarsMap) (*v1.RadixDeployment, error) { +func ConstructForTargetEnvironment(config *v1.RadixApplication, jobName string, imageTag string, branch string, componentImages pipeline.DeployComponentImages, env string, defaultEnvVars v1.EnvVarsMap, radixConfigHash, buildSecretHash string) (*v1.RadixDeployment, error) { commitID := defaultEnvVars[defaults.RadixCommitHashEnvironmentVariable] gitTags := defaultEnvVars[defaults.RadixGitTagsEnvironmentVariable] components, err := GetRadixComponentsForEnv(config, env, componentImages, defaultEnvVars) @@ -105,7 +105,7 @@ func ConstructForTargetEnvironment(config *v1.RadixApplication, jobName string, if err != nil { return nil, err } - radixDeployment := constructRadixDeployment(config, env, jobName, imageTag, branch, commitID, gitTags, components, jobs) + radixDeployment := constructRadixDeployment(config, env, jobName, imageTag, branch, commitID, gitTags, components, jobs, radixConfigHash, buildSecretHash) return radixDeployment, nil } @@ -478,13 +478,20 @@ func (deploy *Deployment) garbageCollectAuxiliaryResources() error { return nil } -func constructRadixDeployment(radixApplication *v1.RadixApplication, env, jobName, imageTag, branch, commitID, gitTags string, components []v1.RadixDeployComponent, jobs []v1.RadixDeployJobComponent) *v1.RadixDeployment { +func constructRadixDeployment(radixApplication *v1.RadixApplication, env, jobName, imageTag, branch, commitID, gitTags string, components []v1.RadixDeployComponent, jobs []v1.RadixDeployJobComponent, radixConfigHash, buildSecretHash string) *v1.RadixDeployment { appName := radixApplication.GetName() deployName := utils.GetDeploymentName(appName, env, imageTag) imagePullSecrets := []corev1.LocalObjectReference{} if len(radixApplication.Spec.PrivateImageHubs) > 0 { imagePullSecrets = append(imagePullSecrets, corev1.LocalObjectReference{Name: defaults.PrivateImageHubSecretName}) } + annotations := map[string]string{ + kube.RadixBranchAnnotation: branch, + kube.RadixGitTagsAnnotation: gitTags, + kube.RadixCommitAnnotation: commitID, + kube.RadixBuildSecretHash: buildSecretHash, + kube.RadixConfigHash: radixConfigHash, + } radixDeployment := &v1.RadixDeployment{ ObjectMeta: metav1.ObjectMeta{ @@ -496,11 +503,7 @@ func constructRadixDeployment(radixApplication *v1.RadixApplication, env, jobNam kube.RadixCommitLabel: commitID, kube.RadixJobNameLabel: jobName, }, - Annotations: map[string]string{ - kube.RadixBranchAnnotation: branch, - kube.RadixGitTagsAnnotation: gitTags, - kube.RadixCommitAnnotation: commitID, - }, + Annotations: annotations, }, Spec: v1.RadixDeploymentSpec{ AppName: appName, diff --git a/pkg/apis/deployment/deployment_test.go b/pkg/apis/deployment/deployment_test.go index 6771eb12d..4b3a9b9bb 100644 --- a/pkg/apis/deployment/deployment_test.go +++ b/pkg/apis/deployment/deployment_test.go @@ -2039,8 +2039,8 @@ func TestConstructForTargetEnvironment_PicksTheCorrectEnvironmentConfig(t *testi {"dev", 3, "db-dev", "9876", "64Mi", "250m", "32Mi", "125m", 2, "plksmfnwi2309", "v1 v2 v1.1", true}, } - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: "anyImage", ImagePath: "anyImagePath"} + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: "anyImagePath"} for _, testcase := range testScenarios { t.Run(testcase.environment, func(t *testing.T) { @@ -2049,7 +2049,7 @@ func TestConstructForTargetEnvironment_PicksTheCorrectEnvironmentConfig(t *testi envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = testcase.expectedGitCommitHash envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = testcase.expectedGitTags - rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, testcase.environment, envVarsMap) + rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, testcase.environment, envVarsMap, "anyhash", "anybuildsecrethash") require.NoError(t, err) assert.Equal(t, testcase.expectedReplicas, *rd.Spec.Components[0].Replicas, "Number of replicas wasn't as expected") @@ -2106,14 +2106,14 @@ func TestConstructForTargetEnvironment_AlwaysPullImageOnDeployOverride(t *testin WithReplicas(test.IntPtr(3)))). BuildRA() - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: "anyImage", ImagePath: "anyImagePath"} + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: "anyImagePath"} envVarsMap := make(v1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" - rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, "dev", envVarsMap) + rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, "dev", envVarsMap, "anyhash", "anybuildsecrethash") require.NoError(t, err) t.Log(rd.Spec.Components[0].Name) @@ -2123,7 +2123,7 @@ func TestConstructForTargetEnvironment_AlwaysPullImageOnDeployOverride(t *testin t.Log(rd.Spec.Components[2].Name) assert.False(t, rd.Spec.Components[2].AlwaysPullImageOnDeploy) - rd, err = ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, "prod", envVarsMap) + rd, err = ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, "prod", envVarsMap, "anyhash", "anybuildsecrethash") require.NoError(t, err) t.Log(rd.Spec.Components[0].Name) @@ -2138,14 +2138,14 @@ func TestConstructForTargetEnvironment_GetCommitID(t *testing.T) { WithComponents(utils.AnApplicationComponent().WithName("app")). BuildRA() - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: "anyImage", ImagePath: "anyImagePath"} + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: "anyImagePath"} envVarsMap := make(v1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "commit-abc" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" - rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, "dev", envVarsMap) + rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimageTag", "anybranch", componentImages, "dev", envVarsMap, "anyhash", "anybuildsecrethash") require.NoError(t, err) assert.Equal(t, "commit-abc", rd.ObjectMeta.Labels[kube.RadixCommitLabel]) @@ -4134,7 +4134,7 @@ func Test_ConstructForTargetEnvironment_Identity(t *testing.T) { ) } ra := utils.ARadixApplication().WithComponents(component).BuildRA() - rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimage", "anybranch", make(map[string]pipeline.ComponentImage), envName, make(v1.EnvVarsMap)) + rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimage", "anybranch", make(pipeline.DeployComponentImages), envName, make(v1.EnvVarsMap), "anyhash", "anybuildsecrethash") require.NoError(t, err) assert.Equal(t, scenario.expected, rd.Spec.Components[0].Identity) } @@ -4147,7 +4147,7 @@ func Test_ConstructForTargetEnvironment_Identity(t *testing.T) { ) } ra := utils.ARadixApplication().WithJobComponents(job).BuildRA() - rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimage", "anybranch", make(map[string]pipeline.ComponentImage), envName, make(v1.EnvVarsMap)) + rd, err := ConstructForTargetEnvironment(ra, "anyjob", "anyimage", "anybranch", make(pipeline.DeployComponentImages), envName, make(v1.EnvVarsMap), "anyhash", "anybuildsecrethash") require.NoError(t, err) assert.Equal(t, scenario.expected, rd.Spec.Jobs[0].Identity) } diff --git a/pkg/apis/deployment/radixcommoncomponent.go b/pkg/apis/deployment/radixcommoncomponent.go index dd0c6882a..dbe2d88bc 100644 --- a/pkg/apis/deployment/radixcommoncomponent.go +++ b/pkg/apis/deployment/radixcommoncomponent.go @@ -77,7 +77,7 @@ func getRadixCommonComponentNode(radixComponent v1.RadixCommonComponent, environ return node } -func getImagePath(componentName string, componentImage *pipeline.ComponentImage, environmentSpecificConfig v1.RadixCommonEnvironmentConfig) (string, error) { +func getImagePath(componentName string, componentImage pipeline.DeployComponentImage, environmentSpecificConfig v1.RadixCommonEnvironmentConfig) (string, error) { image := componentImage.ImagePath if componentImage.Build { return image, nil @@ -105,7 +105,7 @@ func errorMissingExpectedDynamicImageTagName(componentName string) error { return fmt.Errorf(fmt.Sprintf("component %s is missing an expected dynamic imageTagName for its image", componentName)) } -func getImageTagName(componentImage *pipeline.ComponentImage, environmentSpecificConfig v1.RadixCommonEnvironmentConfig) string { +func getImageTagName(componentImage pipeline.DeployComponentImage, environmentSpecificConfig v1.RadixCommonEnvironmentConfig) string { if componentImage.ImageTagName != "" { return componentImage.ImageTagName // provided via radix-api build request } diff --git a/pkg/apis/deployment/radixcomponent.go b/pkg/apis/deployment/radixcomponent.go index e579f2e28..91b4737b9 100644 --- a/pkg/apis/deployment/radixcomponent.go +++ b/pkg/apis/deployment/radixcomponent.go @@ -11,7 +11,7 @@ var ( authTransformer mergo.Transformers = mergoutils.CombinedTransformer{Transformers: []mergo.Transformers{mergoutils.BoolPtrTransformer{}}} ) -func GetRadixComponentsForEnv(radixApplication *v1.RadixApplication, env string, componentImages map[string]pipeline.ComponentImage, defaultEnvVars v1.EnvVarsMap) ([]v1.RadixDeployComponent, error) { +func GetRadixComponentsForEnv(radixApplication *v1.RadixApplication, env string, componentImages pipeline.DeployComponentImages, defaultEnvVars v1.EnvVarsMap) ([]v1.RadixDeployComponent, error) { dnsAppAlias := radixApplication.Spec.DNSAppAlias var components []v1.RadixDeployComponent @@ -49,7 +49,7 @@ func GetRadixComponentsForEnv(radixApplication *v1.RadixApplication, env string, } componentImage := componentImages[componentName] - deployComponent.Image, err = getImagePath(componentName, &componentImage, environmentSpecificConfig) + deployComponent.Image, err = getImagePath(componentName, componentImage, environmentSpecificConfig) if err != nil { return nil, err } diff --git a/pkg/apis/deployment/radixcomponent_test.go b/pkg/apis/deployment/radixcomponent_test.go index 5b6adec55..4ea10d24d 100644 --- a/pkg/apis/deployment/radixcomponent_test.go +++ b/pkg/apis/deployment/radixcomponent_test.go @@ -6,7 +6,7 @@ import ( "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/pipeline" - "github.com/equinor/radix-operator/pkg/apis/radix/v1" + radixv1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -32,15 +32,15 @@ type scenarioDef struct { func TestGetAuthenticationForComponent(t *testing.T) { scenarios := []scenarioDef{ {name: "should return nil when component and environment is nil"}, - {name: "should return component when environment is nil", comp: &v1.Authentication{}, expected: &v1.Authentication{}}, - {name: "should return environment when component is nil", env: &v1.Authentication{}, expected: &v1.Authentication{}}, + {name: "should return component when environment is nil", comp: &radixv1.Authentication{}, expected: &radixv1.Authentication{}}, + {name: "should return environment when component is nil", env: &radixv1.Authentication{}, expected: &radixv1.Authentication{}}, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { - comp, _ := scenario.comp.(*v1.Authentication) - env, _ := scenario.env.(*v1.Authentication) - expected, _ := scenario.expected.(*v1.Authentication) + comp, _ := scenario.comp.(*radixv1.Authentication) + env, _ := scenario.env.(*radixv1.Authentication) + expected, _ := scenario.expected.(*radixv1.Authentication) actual, _ := GetAuthenticationForComponent(comp, env) assert.Equal(t, expected, actual) }) @@ -49,87 +49,87 @@ func TestGetAuthenticationForComponent(t *testing.T) { } func TestGetClientCertificateAuthenticationForComponent(t *testing.T) { - verificationOptional := v1.VerificationTypeOptional - verificationOff := v1.VerificationTypeOff + verificationOptional := radixv1.VerificationTypeOptional + verificationOff := radixv1.VerificationTypeOff scenarios := []scenarioDef{ {name: "should return nil when component and environment is nil"}, - {name: "should return component when environment is nil", comp: &v1.ClientCertificate{}, expected: &v1.ClientCertificate{}}, - {name: "should return environment when component is nil", env: &v1.ClientCertificate{}, expected: &v1.ClientCertificate{}}, + {name: "should return component when environment is nil", comp: &radixv1.ClientCertificate{}, expected: &radixv1.ClientCertificate{}}, + {name: "should return environment when component is nil", env: &radixv1.ClientCertificate{}, expected: &radixv1.ClientCertificate{}}, { name: "should use PassCertificateToUpstream from environment", - comp: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, - env: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, - expected: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + comp: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + env: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + expected: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, }, { name: "should use PassCertificateToUpstream from environment", - comp: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, - env: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, - expected: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + comp: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + env: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + expected: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, }, { name: "should use PassCertificateToUpstream from environment", - comp: &v1.ClientCertificate{}, - env: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, - expected: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + comp: &radixv1.ClientCertificate{}, + env: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + expected: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, }, { name: "should use PassCertificateToUpstream from environment", - comp: &v1.ClientCertificate{}, - env: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, - expected: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + comp: &radixv1.ClientCertificate{}, + env: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + expected: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, }, { name: "should use PassCertificateToUpstream from component when not set in environment", - comp: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, - env: &v1.ClientCertificate{}, - expected: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + comp: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + env: &radixv1.ClientCertificate{}, + expected: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, }, { name: "should use PassCertificateToUpstream from component when not set in environment", - comp: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, - env: &v1.ClientCertificate{}, - expected: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + comp: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, + env: &radixv1.ClientCertificate{}, + expected: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(true)}, }, { name: "should use Verification from environment", - comp: &v1.ClientCertificate{Verification: &verificationOff}, - env: &v1.ClientCertificate{Verification: &verificationOptional}, - expected: &v1.ClientCertificate{Verification: &verificationOptional}, + comp: &radixv1.ClientCertificate{Verification: &verificationOff}, + env: &radixv1.ClientCertificate{Verification: &verificationOptional}, + expected: &radixv1.ClientCertificate{Verification: &verificationOptional}, }, { name: "should use Verification from environment", - comp: &v1.ClientCertificate{}, - env: &v1.ClientCertificate{Verification: &verificationOptional}, - expected: &v1.ClientCertificate{Verification: &verificationOptional}, + comp: &radixv1.ClientCertificate{}, + env: &radixv1.ClientCertificate{Verification: &verificationOptional}, + expected: &radixv1.ClientCertificate{Verification: &verificationOptional}, }, { name: "should use Verification from component", - comp: &v1.ClientCertificate{Verification: &verificationOff}, - env: &v1.ClientCertificate{}, - expected: &v1.ClientCertificate{Verification: &verificationOff}, + comp: &radixv1.ClientCertificate{Verification: &verificationOff}, + env: &radixv1.ClientCertificate{}, + expected: &radixv1.ClientCertificate{Verification: &verificationOff}, }, { name: "should use Verification from component and PassCertificateToUpstream from environment", - comp: &v1.ClientCertificate{Verification: &verificationOff, PassCertificateToUpstream: utils.BoolPtr(true)}, - env: &v1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, - expected: &v1.ClientCertificate{Verification: &verificationOff, PassCertificateToUpstream: utils.BoolPtr(false)}, + comp: &radixv1.ClientCertificate{Verification: &verificationOff, PassCertificateToUpstream: utils.BoolPtr(true)}, + env: &radixv1.ClientCertificate{PassCertificateToUpstream: utils.BoolPtr(false)}, + expected: &radixv1.ClientCertificate{Verification: &verificationOff, PassCertificateToUpstream: utils.BoolPtr(false)}, }, { name: "should use Verification from environment and PassCertificateToUpstream from component", - comp: &v1.ClientCertificate{Verification: &verificationOff, PassCertificateToUpstream: utils.BoolPtr(true)}, - env: &v1.ClientCertificate{Verification: &verificationOptional}, - expected: &v1.ClientCertificate{Verification: &verificationOptional, PassCertificateToUpstream: utils.BoolPtr(true)}, + comp: &radixv1.ClientCertificate{Verification: &verificationOff, PassCertificateToUpstream: utils.BoolPtr(true)}, + env: &radixv1.ClientCertificate{Verification: &verificationOptional}, + expected: &radixv1.ClientCertificate{Verification: &verificationOptional, PassCertificateToUpstream: utils.BoolPtr(true)}, }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { - comp, _ := scenario.comp.(*v1.ClientCertificate) - env, _ := scenario.env.(*v1.ClientCertificate) - expected, _ := scenario.expected.(*v1.ClientCertificate) - actual, _ := GetAuthenticationForComponent(&v1.Authentication{ClientCertificate: comp}, &v1.Authentication{ClientCertificate: env}) + comp, _ := scenario.comp.(*radixv1.ClientCertificate) + env, _ := scenario.env.(*radixv1.ClientCertificate) + expected, _ := scenario.expected.(*radixv1.ClientCertificate) + actual, _ := GetAuthenticationForComponent(&radixv1.Authentication{ClientCertificate: comp}, &radixv1.Authentication{ClientCertificate: env}) assert.Equal(t, expected, actual.ClientCertificate) }) @@ -139,40 +139,40 @@ func TestGetClientCertificateAuthenticationForComponent(t *testing.T) { func TestGetOAuth2AuthenticationForComponent(t *testing.T) { scenarios := []scenarioDef{ {name: "should return nil when component and environment is nil"}, - {name: "should return component when environment is nil", comp: &v1.OAuth2{}, expected: &v1.OAuth2{}}, - {name: "should return environment when component is nil", env: &v1.OAuth2{}, expected: &v1.OAuth2{}}, + {name: "should return component when environment is nil", comp: &radixv1.OAuth2{}, expected: &radixv1.OAuth2{}}, + {name: "should return environment when component is nil", env: &radixv1.OAuth2{}, expected: &radixv1.OAuth2{}}, { name: "should override OAuth2 from environment", - comp: &v1.OAuth2{ClientID: "123", Scope: "openid", SetXAuthRequestHeaders: utils.BoolPtr(true), SessionStoreType: "cookie"}, - env: &v1.OAuth2{Scope: "email", SetXAuthRequestHeaders: utils.BoolPtr(false), SetAuthorizationHeader: utils.BoolPtr(true), SessionStoreType: "redis"}, - expected: &v1.OAuth2{ClientID: "123", Scope: "email", SetXAuthRequestHeaders: utils.BoolPtr(false), SetAuthorizationHeader: utils.BoolPtr(true), SessionStoreType: "redis"}, + comp: &radixv1.OAuth2{ClientID: "123", Scope: "openid", SetXAuthRequestHeaders: utils.BoolPtr(true), SessionStoreType: "cookie"}, + env: &radixv1.OAuth2{Scope: "email", SetXAuthRequestHeaders: utils.BoolPtr(false), SetAuthorizationHeader: utils.BoolPtr(true), SessionStoreType: "redis"}, + expected: &radixv1.OAuth2{ClientID: "123", Scope: "email", SetXAuthRequestHeaders: utils.BoolPtr(false), SetAuthorizationHeader: utils.BoolPtr(true), SessionStoreType: "redis"}, }, { name: "should override OAuth2.RedisStore from environment", - comp: &v1.OAuth2{RedisStore: &v1.OAuth2RedisStore{ConnectionURL: "foo"}}, - env: &v1.OAuth2{RedisStore: &v1.OAuth2RedisStore{ConnectionURL: "bar"}}, - expected: &v1.OAuth2{RedisStore: &v1.OAuth2RedisStore{ConnectionURL: "bar"}}, + comp: &radixv1.OAuth2{RedisStore: &radixv1.OAuth2RedisStore{ConnectionURL: "foo"}}, + env: &radixv1.OAuth2{RedisStore: &radixv1.OAuth2RedisStore{ConnectionURL: "bar"}}, + expected: &radixv1.OAuth2{RedisStore: &radixv1.OAuth2RedisStore{ConnectionURL: "bar"}}, }, { name: "should override OAuth2.CookieStore from environment", - comp: &v1.OAuth2{CookieStore: &v1.OAuth2CookieStore{Minimal: utils.BoolPtr(true)}}, - env: &v1.OAuth2{CookieStore: &v1.OAuth2CookieStore{Minimal: utils.BoolPtr(false)}}, - expected: &v1.OAuth2{CookieStore: &v1.OAuth2CookieStore{Minimal: utils.BoolPtr(false)}}, + comp: &radixv1.OAuth2{CookieStore: &radixv1.OAuth2CookieStore{Minimal: utils.BoolPtr(true)}}, + env: &radixv1.OAuth2{CookieStore: &radixv1.OAuth2CookieStore{Minimal: utils.BoolPtr(false)}}, + expected: &radixv1.OAuth2{CookieStore: &radixv1.OAuth2CookieStore{Minimal: utils.BoolPtr(false)}}, }, { name: "should override OAuth2.Cookie from environment", - comp: &v1.OAuth2{Cookie: &v1.OAuth2Cookie{Name: "oauth", Expire: "1h"}}, - env: &v1.OAuth2{Cookie: &v1.OAuth2Cookie{Name: "_oauth", Refresh: "2h"}}, - expected: &v1.OAuth2{Cookie: &v1.OAuth2Cookie{Name: "_oauth", Expire: "1h", Refresh: "2h"}}, + comp: &radixv1.OAuth2{Cookie: &radixv1.OAuth2Cookie{Name: "oauth", Expire: "1h"}}, + env: &radixv1.OAuth2{Cookie: &radixv1.OAuth2Cookie{Name: "_oauth", Refresh: "2h"}}, + expected: &radixv1.OAuth2{Cookie: &radixv1.OAuth2Cookie{Name: "_oauth", Expire: "1h", Refresh: "2h"}}, }, } for _, scenario := range scenarios { t.Run(scenario.name, func(t *testing.T) { - comp, _ := scenario.comp.(*v1.OAuth2) - env, _ := scenario.env.(*v1.OAuth2) - expected, _ := scenario.expected.(*v1.OAuth2) - actual, _ := GetAuthenticationForComponent(&v1.Authentication{OAuth2: comp}, &v1.Authentication{OAuth2: env}) + comp, _ := scenario.comp.(*radixv1.OAuth2) + env, _ := scenario.env.(*radixv1.OAuth2) + expected, _ := scenario.expected.(*radixv1.OAuth2) + actual, _ := GetAuthenticationForComponent(&radixv1.Authentication{OAuth2: comp}, &radixv1.Authentication{OAuth2: env}) assert.Equal(t, expected, actual.OAuth2) }) } @@ -187,9 +187,9 @@ func TestGetRadixComponentsForEnv_PublicPort_OldPublic(t *testing.T) { WithPort("http", 80). WithPort("https", 443)).BuildRA() - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -243,9 +243,9 @@ func TestGetRadixComponentsForEnv_PublicPort_OldPublic(t *testing.T) { } func TestGetRadixComponentsForEnv_ListOfExternalAliasesForComponent_GetListOfAliases(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -276,9 +276,9 @@ func TestGetRadixComponentsForEnv_ListOfExternalAliasesForComponent_GetListOfAli } func TestGetRadixComponentsForEnv_CommonEnvironmentVariables_No_Override(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -335,9 +335,9 @@ func TestGetRadixComponentsForEnv_CommonEnvironmentVariables_No_Override(t *test } func TestGetRadixComponentsForEnv_CommonEnvironmentVariables_With_Override(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -399,9 +399,9 @@ func TestGetRadixComponentsForEnv_CommonEnvironmentVariables_With_Override(t *te } func TestGetRadixComponentsForEnv_CommonEnvironmentVariables_NilVariablesMapInEnvConfig(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -452,13 +452,13 @@ func TestGetRadixComponentsForEnv_CommonEnvironmentVariables_NilVariablesMapInEn func TestGetRadixComponentsForEnv_Monitoring(t *testing.T) { envs := [2]string{"prod", "dev"} - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" - monitoringConfig := v1.MonitoringConfig{ + monitoringConfig := radixv1.MonitoringConfig{ PortName: "monitor", Path: "/api/monitor", } @@ -510,9 +510,9 @@ func TestGetRadixComponentsForEnv_Monitoring(t *testing.T) { } func TestGetRadixComponentsForEnv_CommonResources(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -557,9 +557,9 @@ func TestGetRadixComponentsForEnv_CommonResources(t *testing.T) { } func Test_GetRadixComponents_NodeName(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" compGpu := "comp gpu" @@ -572,17 +572,17 @@ func Test_GetRadixComponents_NodeName(t *testing.T) { WithComponents( utils.AnApplicationComponent(). WithName("comp"). - WithNode(v1.RadixNode{Gpu: compGpu, GpuCount: compGpuCount}). + WithNode(radixv1.RadixNode{Gpu: compGpu, GpuCount: compGpuCount}). WithEnvironmentConfigs( utils.AnEnvironmentConfig(). WithEnvironment("env1"). - WithNode(v1.RadixNode{Gpu: envGpu1, GpuCount: envGpuCount1}), + WithNode(radixv1.RadixNode{Gpu: envGpu1, GpuCount: envGpuCount1}), utils.AnEnvironmentConfig(). WithEnvironment("env2"). - WithNode(v1.RadixNode{GpuCount: envGpuCount2}), + WithNode(radixv1.RadixNode{GpuCount: envGpuCount2}), utils.AnEnvironmentConfig(). WithEnvironment("env3"). - WithNode(v1.RadixNode{Gpu: envGpu3}), + WithNode(radixv1.RadixNode{Gpu: envGpu3}), utils.AnEnvironmentConfig(). WithEnvironment("env4"), ), @@ -615,9 +615,9 @@ func Test_GetRadixComponents_NodeName(t *testing.T) { } func TestGetRadixComponentsForEnv_ReturnsOnlyNotDisabledComponents(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -683,9 +683,9 @@ func TestGetRadixComponentsForEnv_ReturnsOnlyNotDisabledComponents(t *testing.T) } func TestGetRadixComponentsForEnv_ReturnsOnlyNotDisabledJobComponents(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["app"] = pipeline.ComponentImage{ImageName: anyImage, ImagePath: anyImagePath} - envVarsMap := make(v1.EnvVarsMap) + componentImages := make(pipeline.DeployComponentImages) + componentImages["app"] = pipeline.DeployComponentImage{ImagePath: anyImagePath} + envVarsMap := make(radixv1.EnvVarsMap) envVarsMap[defaults.RadixCommitHashEnvironmentVariable] = "anycommit" envVarsMap[defaults.RadixGitTagsEnvironmentVariable] = "anytag" @@ -755,26 +755,26 @@ func TestGetRadixComponentsForEnv_ReturnsOnlyNotDisabledJobComponents(t *testing func Test_GetRadixComponentsForEnv_Identity(t *testing.T) { type scenarioSpec struct { name string - commonConfig *v1.Identity + commonConfig *radixv1.Identity configureEnvironment bool - environmentConfig *v1.Identity - expected *v1.Identity + environmentConfig *radixv1.Identity + expected *radixv1.Identity } scenarios := []scenarioSpec{ - {name: "nil when commonConfig and environmentConfig is empty", commonConfig: &v1.Identity{}, configureEnvironment: true, environmentConfig: &v1.Identity{}, expected: nil}, - {name: "nil when commonConfig is nil and environmentConfig is empty", commonConfig: nil, configureEnvironment: true, environmentConfig: &v1.Identity{}, expected: nil}, - {name: "nil when commonConfig is empty and environmentConfig is nil", commonConfig: &v1.Identity{}, configureEnvironment: true, environmentConfig: nil, expected: nil}, + {name: "nil when commonConfig and environmentConfig is empty", commonConfig: &radixv1.Identity{}, configureEnvironment: true, environmentConfig: &radixv1.Identity{}, expected: nil}, + {name: "nil when commonConfig is nil and environmentConfig is empty", commonConfig: nil, configureEnvironment: true, environmentConfig: &radixv1.Identity{}, expected: nil}, + {name: "nil when commonConfig is empty and environmentConfig is nil", commonConfig: &radixv1.Identity{}, configureEnvironment: true, environmentConfig: nil, expected: nil}, {name: "nil when commonConfig is nil and environmentConfig is not set", commonConfig: nil, configureEnvironment: false, environmentConfig: nil, expected: nil}, - {name: "nil when commonConfig is empty and environmentConfig is not set", commonConfig: &v1.Identity{}, configureEnvironment: false, environmentConfig: nil, expected: nil}, - {name: "use commonConfig when environmentConfig is empty", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}, configureEnvironment: true, environmentConfig: &v1.Identity{}, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, - {name: "use commonConfig when environmentConfig.Azure is empty", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}, configureEnvironment: true, environmentConfig: &v1.Identity{Azure: &v1.AzureIdentity{}}, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, - {name: "override non-empty commonConfig with environmentConfig.Azure", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}, configureEnvironment: true, environmentConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}}, - {name: "override empty commonConfig with environmentConfig", commonConfig: &v1.Identity{}, configureEnvironment: true, environmentConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}}, - {name: "override empty commonConfig.Azure with environmentConfig", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{}}, configureEnvironment: true, environmentConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}}, - {name: "transform clientId with curly to standard format", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "{11111111-2222-3333-4444-555555555555}"}}, configureEnvironment: false, environmentConfig: nil, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, - {name: "transform clientId with urn:uuid to standard format", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "urn:uuid:11111111-2222-3333-4444-555555555555"}}, configureEnvironment: false, environmentConfig: nil, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, - {name: "transform clientId without dashes to standard format", commonConfig: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111222233334444555555555555"}}, configureEnvironment: false, environmentConfig: nil, expected: &v1.Identity{Azure: &v1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, + {name: "nil when commonConfig is empty and environmentConfig is not set", commonConfig: &radixv1.Identity{}, configureEnvironment: false, environmentConfig: nil, expected: nil}, + {name: "use commonConfig when environmentConfig is empty", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}, configureEnvironment: true, environmentConfig: &radixv1.Identity{}, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, + {name: "use commonConfig when environmentConfig.Azure is empty", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}, configureEnvironment: true, environmentConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{}}, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, + {name: "override non-empty commonConfig with environmentConfig.Azure", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}, configureEnvironment: true, environmentConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}}, + {name: "override empty commonConfig with environmentConfig", commonConfig: &radixv1.Identity{}, configureEnvironment: true, environmentConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}}, + {name: "override empty commonConfig.Azure with environmentConfig", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{}}, configureEnvironment: true, environmentConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "66666666-7777-8888-9999-aaaaaaaaaaaa"}}}, + {name: "transform clientId with curly to standard format", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "{11111111-2222-3333-4444-555555555555}"}}, configureEnvironment: false, environmentConfig: nil, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, + {name: "transform clientId with urn:uuid to standard format", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "urn:uuid:11111111-2222-3333-4444-555555555555"}}, configureEnvironment: false, environmentConfig: nil, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, + {name: "transform clientId without dashes to standard format", commonConfig: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111222233334444555555555555"}}, configureEnvironment: false, environmentConfig: nil, expected: &radixv1.Identity{Azure: &radixv1.AzureIdentity{ClientId: "11111111-2222-3333-4444-555555555555"}}}, } for _, scenario := range scenarios { @@ -788,7 +788,7 @@ func Test_GetRadixComponentsForEnv_Identity(t *testing.T) { } ra := utils.ARadixApplication().WithComponents(component).BuildRA() sut := GetRadixComponentsForEnv - components, err := sut(ra, envName, make(map[string]pipeline.ComponentImage), make(v1.EnvVarsMap)) + components, err := sut(ra, envName, make(pipeline.DeployComponentImages), make(radixv1.EnvVarsMap)) require.NoError(t, err) assert.Equal(t, scenario.expected, components[0].Identity) }) @@ -887,10 +887,10 @@ func TestGetRadixComponentsForEnv_ImageWithImageTagName(t *testing.T) { for _, ts := range scenarios { t.Run(ts.name, func(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) + componentImages := make(pipeline.DeployComponentImages) var componentBuilders []utils.RadixApplicationComponentBuilder for _, componentName := range []string{componentName1, componentName2} { - componentImages[componentName] = pipeline.ComponentImage{ImageName: ts.componentImages[componentName], ImagePath: ts.componentImages[componentName], ImageTagName: ts.externalImageTagNames[componentName]} + componentImages[componentName] = pipeline.DeployComponentImage{ImagePath: ts.componentImages[componentName], ImageTagName: ts.externalImageTagNames[componentName]} componentBuilder := utils.NewApplicationComponentBuilder() componentBuilder.WithName(componentName).WithImage(ts.componentImages[componentName]). WithEnvironmentConfig(utils.NewComponentEnvironmentBuilder().WithEnvironment(environment).WithImageTagName(ts.environmentConfigImageTagNames[componentName])) @@ -899,7 +899,7 @@ func TestGetRadixComponentsForEnv_ImageWithImageTagName(t *testing.T) { ra := utils.ARadixApplication().WithEnvironment(environment, "master").WithComponents(componentBuilders...).BuildRA() - deployComponents, err := GetRadixComponentsForEnv(ra, environment, componentImages, make(v1.EnvVarsMap)) + deployComponents, err := GetRadixComponentsForEnv(ra, environment, componentImages, make(radixv1.EnvVarsMap)) if err != nil && ts.expectedError == nil { assert.Fail(t, fmt.Sprintf("unexpected error %v", err)) return @@ -923,7 +923,7 @@ func TestGetRadixComponentsForEnv_ImageWithImageTagName(t *testing.T) { } } -func convertRadixDeployComponentToNameSet(deployComponents []v1.RadixDeployComponent) map[string]bool { +func convertRadixDeployComponentToNameSet(deployComponents []radixv1.RadixDeployComponent) map[string]bool { set := make(map[string]bool) for _, deployComponent := range deployComponents { set[deployComponent.Name] = true @@ -931,7 +931,7 @@ func convertRadixDeployComponentToNameSet(deployComponents []v1.RadixDeployCompo return set } -func convertRadixDeployJobComponentsToNameSet(deployComponents []v1.RadixDeployJobComponent) map[string]bool { +func convertRadixDeployJobComponentsToNameSet(deployComponents []radixv1.RadixDeployJobComponent) map[string]bool { set := make(map[string]bool) for _, deployComponent := range deployComponents { set[deployComponent.Name] = true diff --git a/pkg/apis/deployment/radixjobcomponent.go b/pkg/apis/deployment/radixjobcomponent.go index 8a6c2581c..9f869d283 100644 --- a/pkg/apis/deployment/radixjobcomponent.go +++ b/pkg/apis/deployment/radixjobcomponent.go @@ -16,12 +16,12 @@ type JobComponentsBuilder interface { type jobComponentsBuilder struct { ra *v1.RadixApplication env string - componentImages map[string]pipeline.ComponentImage + componentImages pipeline.DeployComponentImages defaultEnvVars v1.EnvVarsMap } // NewJobComponentsBuilder constructor for JobComponentsBuilder -func NewJobComponentsBuilder(ra *v1.RadixApplication, env string, componentImages map[string]pipeline.ComponentImage, defaultEnvVars v1.EnvVarsMap) JobComponentsBuilder { +func NewJobComponentsBuilder(ra *v1.RadixApplication, env string, componentImages pipeline.DeployComponentImages, defaultEnvVars v1.EnvVarsMap) JobComponentsBuilder { return &jobComponentsBuilder{ ra: ra, env: env, @@ -78,7 +78,7 @@ func (c *jobComponentsBuilder) buildJobComponent(radixJobComponent v1.RadixJobCo return nil, commonErrors.Concat(errs) } - image, err := getImagePath(componentName, &componentImage, environmentSpecificConfig) + image, err := getImagePath(componentName, componentImage, environmentSpecificConfig) if err != nil { return nil, err } diff --git a/pkg/apis/deployment/radixjobcomponent_test.go b/pkg/apis/deployment/radixjobcomponent_test.go index 9dbca6aeb..66cf2182e 100644 --- a/pkg/apis/deployment/radixjobcomponent_test.go +++ b/pkg/apis/deployment/radixjobcomponent_test.go @@ -2,9 +2,10 @@ package deployment import ( "fmt" - "github.com/equinor/radix-common/utils/pointers" "testing" + "github.com/equinor/radix-common/utils/pointers" + "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/utils/numbers" @@ -30,7 +31,7 @@ func Test_GetRadixJobComponents_BuildAllJobComponents(t *testing.T) { cfg := jobComponentsBuilder{ ra: ra, env: "any", - componentImages: make(map[string]pipeline.ComponentImage), + componentImages: make(pipeline.DeployComponentImages), } jobs, err := cfg.JobComponents() require.NoError(t, err) @@ -60,7 +61,7 @@ func Test_GetRadixJobComponentsWithNode_BuildAllJobComponents(t *testing.T) { cfg := jobComponentsBuilder{ ra: ra, env: "any", - componentImages: make(map[string]pipeline.ComponentImage), + componentImages: make(pipeline.DeployComponentImages), } jobs, err := cfg.JobComponents() require.NoError(t, err) @@ -95,7 +96,7 @@ func Test_GetRadixJobComponents_EnvironmentVariables(t *testing.T) { cfg := jobComponentsBuilder{ ra: ra, env: "env1", - componentImages: make(map[string]pipeline.ComponentImage), + componentImages: make(pipeline.DeployComponentImages), defaultEnvVars: envVarsMap, } jobComponents, err := cfg.JobComponents() @@ -138,7 +139,7 @@ func Test_GetRadixJobComponents_Monitoring(t *testing.T) { ), ).BuildRA() - cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(map[string]pipeline.ComponentImage)} + cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(pipeline.DeployComponentImages)} jobComponents, err := cfg.JobComponents() require.NoError(t, err) job := jobComponents[0] @@ -146,7 +147,7 @@ func Test_GetRadixJobComponents_Monitoring(t *testing.T) { assert.Empty(t, job.MonitoringConfig.PortName) assert.Empty(t, job.MonitoringConfig.Path) - cfg = jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(map[string]pipeline.ComponentImage)} + cfg = jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(pipeline.DeployComponentImages)} jobComponents, err = cfg.JobComponents() require.NoError(t, err) job = jobComponents[0] @@ -154,7 +155,7 @@ func Test_GetRadixJobComponents_Monitoring(t *testing.T) { assert.Empty(t, job.MonitoringConfig.PortName) assert.Empty(t, job.MonitoringConfig.Path) - cfg = jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(map[string]pipeline.ComponentImage)} + cfg = jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(pipeline.DeployComponentImages)} jobComponents, err = cfg.JobComponents() require.NoError(t, err) job = jobComponents[1] @@ -162,7 +163,7 @@ func Test_GetRadixJobComponents_Monitoring(t *testing.T) { assert.Equal(t, monitoringConfig.PortName, job.MonitoringConfig.PortName) assert.Equal(t, monitoringConfig.Path, job.MonitoringConfig.Path) - cfg = jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(map[string]pipeline.ComponentImage)} + cfg = jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(pipeline.DeployComponentImages)} jobComponents, err = cfg.JobComponents() require.NoError(t, err) job = jobComponents[1] @@ -206,7 +207,7 @@ func Test_GetRadixJobComponents_Identity(t *testing.T) { ) } ra := utils.ARadixApplication().WithJobComponents(jobComponent).BuildRA() - sut := jobComponentsBuilder{ra: ra, env: envName, componentImages: make(map[string]pipeline.ComponentImage)} + sut := jobComponentsBuilder{ra: ra, env: envName, componentImages: make(pipeline.DeployComponentImages)} jobs, err := sut.JobComponents() require.NoError(t, err) assert.Equal(t, scenario.expected, jobs[0].Identity) @@ -215,9 +216,9 @@ func Test_GetRadixJobComponents_Identity(t *testing.T) { } func Test_GetRadixJobComponents_ImageTagName(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) - componentImages["job"] = pipeline.ComponentImage{Build: false, ImagePath: "img:{imageTagName}"} - componentImages["job2"] = pipeline.ComponentImage{ImagePath: "job2:tag"} + componentImages := make(pipeline.DeployComponentImages) + componentImages["job"] = pipeline.DeployComponentImage{Build: false, ImagePath: "img:{imageTagName}"} + componentImages["job2"] = pipeline.DeployComponentImage{ImagePath: "job2:tag"} ra := utils.ARadixApplication(). WithJobComponents( @@ -330,7 +331,7 @@ func Test_GetRadixJobComponents_Resources(t *testing.T) { WithName("job2"), ).BuildRA() - cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(map[string]pipeline.ComponentImage)} + cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(pipeline.DeployComponentImages)} jobs, err := cfg.JobComponents() require.NoError(t, err) assert.Equal(t, "1100Mi", jobs[0].Resources.Requests["memory"]) @@ -339,7 +340,7 @@ func Test_GetRadixJobComponents_Resources(t *testing.T) { assert.Equal(t, "1250m", jobs[0].Resources.Limits["cpu"]) assert.Empty(t, jobs[1].Resources) - cfg = jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(map[string]pipeline.ComponentImage)} + cfg = jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(pipeline.DeployComponentImages)} jobs, err = cfg.JobComponents() require.NoError(t, err) assert.Equal(t, "100Mi", jobs[0].Resources.Requests["memory"]) @@ -359,7 +360,7 @@ func Test_GetRadixJobComponents_Ports(t *testing.T) { WithName("job2"), ).BuildRA() - cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(map[string]pipeline.ComponentImage)} + cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(pipeline.DeployComponentImages)} jobs, err := cfg.JobComponents() require.NoError(t, err) assert.Len(t, jobs[0].Ports, 2) @@ -382,7 +383,7 @@ func Test_GetRadixJobComponents_Secrets(t *testing.T) { WithName("job2"), ).BuildRA() - cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(map[string]pipeline.ComponentImage)} + cfg := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(pipeline.DeployComponentImages)} jobs, err := cfg.JobComponents() require.NoError(t, err) assert.Len(t, jobs[0].Secrets, 2) @@ -408,8 +409,8 @@ func Test_GetRadixJobComponents_TimeLimitSeconds(t *testing.T) { ), ).BuildRA() - cfgEnv1 := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(map[string]pipeline.ComponentImage)} - cfgEnv2 := jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(map[string]pipeline.ComponentImage)} + cfgEnv1 := jobComponentsBuilder{ra: ra, env: "env1", componentImages: make(pipeline.DeployComponentImages)} + cfgEnv2 := jobComponentsBuilder{ra: ra, env: "env2", componentImages: make(pipeline.DeployComponentImages)} env1Job, err := cfgEnv1.JobComponents() require.NoError(t, err) env2Job, err := cfgEnv2.JobComponents() @@ -450,13 +451,13 @@ func Test_GetRadixJobComponents_BackoffLimit(t *testing.T) { ). BuildRA() - devBuilder := jobComponentsBuilder{ra: ra, env: devEnvName, componentImages: map[string]pipeline.ComponentImage{}} + devBuilder := jobComponentsBuilder{ra: ra, env: devEnvName, componentImages: pipeline.DeployComponentImages{}} devJobs, err := devBuilder.JobComponents() require.NoError(t, err, "devJobs build error") require.Len(t, devJobs, 1, "devJobs length") assert.Equal(t, scenario.expectedEnvDevBackoffLimit, devJobs[0].BackoffLimit, "devJobs backoffLimit") - prodBuilder := jobComponentsBuilder{ra: ra, env: prodEnvName, componentImages: map[string]pipeline.ComponentImage{}} + prodBuilder := jobComponentsBuilder{ra: ra, env: prodEnvName, componentImages: pipeline.DeployComponentImages{}} prodJobs, err := prodBuilder.JobComponents() require.NoError(t, err, "prodJobs build error") require.Len(t, prodJobs, 1, "prodJobs length") @@ -497,7 +498,7 @@ func Test_GetRadixJobComponents_Notifications(t *testing.T) { ) } ra := utils.ARadixApplication().WithJobComponents(jobComponent).BuildRA() - sut := jobComponentsBuilder{ra: ra, env: envName, componentImages: make(map[string]pipeline.ComponentImage)} + sut := jobComponentsBuilder{ra: ra, env: envName, componentImages: make(pipeline.DeployComponentImages)} jobs, err := sut.JobComponents() require.NoError(t, err) assert.Equal(t, scenario.expected, jobs[0].Notifications) @@ -597,10 +598,10 @@ func TestGetRadixJobComponentsForEnv_ImageWithImageTagName(t *testing.T) { for _, ts := range scenarios { t.Run(ts.name, func(t *testing.T) { - componentImages := make(map[string]pipeline.ComponentImage) + componentImages := make(pipeline.DeployComponentImages) var componentBuilders []utils.RadixApplicationJobComponentBuilder for _, jobComponentName := range []string{componentName1, componentName2} { - componentImages[jobComponentName] = pipeline.ComponentImage{ImageName: ts.componentImages[jobComponentName], ImagePath: ts.componentImages[jobComponentName], ImageTagName: ts.externalImageTagNames[jobComponentName]} + componentImages[jobComponentName] = pipeline.DeployComponentImage{ImagePath: ts.componentImages[jobComponentName], ImageTagName: ts.externalImageTagNames[jobComponentName]} componentBuilder := utils.NewApplicationJobComponentBuilder() componentBuilder.WithName(jobComponentName).WithImage(ts.componentImages[jobComponentName]). WithEnvironmentConfig(utils.NewJobComponentEnvironmentBuilder().WithEnvironment(environment).WithImageTagName(ts.environmentConfigImageTagNames[jobComponentName])) diff --git a/pkg/apis/deployment/secretrefs_test.go b/pkg/apis/deployment/secretrefs_test.go index 5cb08f22c..803acfb5a 100644 --- a/pkg/apis/deployment/secretrefs_test.go +++ b/pkg/apis/deployment/secretrefs_test.go @@ -12,7 +12,7 @@ import ( "github.com/equinor/radix-operator/pkg/apis/defaults" "github.com/equinor/radix-operator/pkg/apis/kube" "github.com/equinor/radix-operator/pkg/apis/pipeline" - "github.com/equinor/radix-operator/pkg/apis/radix/v1" + v1 "github.com/equinor/radix-operator/pkg/apis/radix/v1" "github.com/equinor/radix-operator/pkg/apis/test" "github.com/equinor/radix-operator/pkg/apis/utils" "github.com/stretchr/testify/assert" @@ -323,7 +323,7 @@ func Test_GetRadixComponentsForEnv_AzureKeyVault(t *testing.T) { } ra := utils.ARadixApplication().WithComponents(component).BuildRA() sut := GetRadixComponentsForEnv - components, err := sut(ra, envName, make(map[string]pipeline.ComponentImage), make(v1.EnvVarsMap)) + components, err := sut(ra, envName, make(pipeline.DeployComponentImages), make(v1.EnvVarsMap)) require.NoError(t, err) azureKeyVaults := components[0].SecretRefs.AzureKeyVaults assert.EqualValues(t, sortAzureKeyVaults(scenario.expected), sortAzureKeyVaults(azureKeyVaults)) @@ -372,7 +372,7 @@ func Test_GetRadixComponentsForEnv_AzureKeyVaultUseIAzureIdentity(t *testing.T) } ra := utils.ARadixApplication().WithComponents(component).BuildRA() sut := GetRadixComponentsForEnv - components, err := sut(ra, envName, make(map[string]pipeline.ComponentImage), make(v1.EnvVarsMap)) + components, err := sut(ra, envName, make(pipeline.DeployComponentImages), make(v1.EnvVarsMap)) require.NoError(t, err) azureKeyVaults := components[0].SecretRefs.AzureKeyVaults if len(azureKeyVaults) == 0 { diff --git a/pkg/apis/job/job.go b/pkg/apis/job/job.go index 08bb21b9d..db24a33a1 100644 --- a/pkg/apis/job/job.go +++ b/pkg/apis/job/job.go @@ -463,7 +463,7 @@ func (job *Job) getJobStepsBuildPipeline(pipelinePod *corev1.Pod, pipelineJob *b steps = append(steps, getJobStepWithNoComponents(pod.GetName(), &containerStatus)) } - componentImages := make(map[string]pipeline.ComponentImage) + componentImages := make(pipeline.BuildComponentImages) if err := getObjectFromJobAnnotation(&jobStep, kube.RadixComponentImagesAnnotation, &componentImages); err != nil { return nil, err } @@ -492,7 +492,7 @@ func getObjectFromJobAnnotation(job *batchv1.Job, annotationName string, obj int return nil } -func getComponentsForContainer(name string, componentImages map[string]pipeline.ComponentImage) []string { +func getComponentsForContainer(name string, componentImages pipeline.BuildComponentImages) []string { components := make([]string, 0) for component, componentImage := range componentImages { diff --git a/pkg/apis/job/job_steps_test.go b/pkg/apis/job/job_steps_test.go index 64f8dc6fa..2163842e7 100644 --- a/pkg/apis/job/job_steps_test.go +++ b/pkg/apis/job/job_steps_test.go @@ -179,7 +179,7 @@ func (s *RadixJobStepTestSuite) Test_StatusSteps_BuildSteps() { jobs: []*batchv1.Job{ s.getPipelineJob("job-5", "app-5", "a_tag"), s.getPreparePipelineJob("prepare-pipeline-5", "job-5", "app-5", "a_tag"), - s.getBuildJob("build-job-5", "job-5", "app-5", "a_tag", map[string]pipeline.ComponentImage{ + s.getBuildJob("build-job-5", "job-5", "app-5", "a_tag", pipeline.BuildComponentImages{ "comp": {ContainerName: "build-app"}, "multi1": {ContainerName: "build-multi"}, "multi3": {ContainerName: "build-multi"}, @@ -227,7 +227,7 @@ func (s *RadixJobStepTestSuite) Test_StatusSteps_InitContainers() { jobs: []*batchv1.Job{ s.getPipelineJob("job-1", "app-1", "a_tag"), s.getPreparePipelineJob("prepare-pipeline-1", "job-1", "app-1", "a_tag"), - s.getBuildJob("build-job-1", "job-1", "app-1", "a_tag", map[string]pipeline.ComponentImage{}), + s.getBuildJob("build-job-1", "job-1", "app-1", "a_tag", pipeline.BuildComponentImages{}), s.getRunPipelineJob("run-pipeline-1", "job-1", "app-1", "a_tag"), }, pods: []*corev1.Pod{ @@ -356,7 +356,7 @@ func (s *RadixJobStepTestSuite) getRunPipelineJob(name, radixJobName, appName, i } } -func (s *RadixJobStepTestSuite) getBuildJob(name, radixJobName, appName, imageTag string, componentImages map[string]pipeline.ComponentImage) *batchv1.Job { +func (s *RadixJobStepTestSuite) getBuildJob(name, radixJobName, appName, imageTag string, componentImages pipeline.BuildComponentImages) *batchv1.Job { componentImageAnnotation, _ := json.Marshal(&componentImages) return &batchv1.Job{ diff --git a/pkg/apis/kube/kube.go b/pkg/apis/kube/kube.go index 05b4672d5..a6ef72d95 100644 --- a/pkg/apis/kube/kube.go +++ b/pkg/apis/kube/kube.go @@ -20,6 +20,8 @@ const ( RadixBranchAnnotation = "radix-branch" RadixGitTagsAnnotation = "radix.equinor.com/radix-git-tags" RadixCommitAnnotation = "radix.equinor.com/radix-commit" + RadixConfigHash = "radix.equinor.com/radix-config-hash" + RadixBuildSecretHash = "radix.equinor.com/build-secret-hash" RadixComponentImagesAnnotation = "radix-component-images" RadixDeploymentNameAnnotation = "radix-deployment-name" RadixDeploymentPromotedFromDeploymentAnnotation = "radix.equinor.com/radix-deployment-promoted-from-deployment" diff --git a/pkg/apis/kube/radix_deployment.go b/pkg/apis/kube/radix_deployment.go index fe6d7fb9b..282097363 100644 --- a/pkg/apis/kube/radix_deployment.go +++ b/pkg/apis/kube/radix_deployment.go @@ -50,15 +50,15 @@ func (kubeutil *Kube) ListRadixDeployments(namespace string) ([]*v1.RadixDeploym } -//GetActiveDeployment Get active RadixDeployment for the namespace +// GetActiveDeployment Get active RadixDeployment for the namespace func (kubeutil *Kube) GetActiveDeployment(namespace string) (*v1.RadixDeployment, error) { - radixDeployments, err := kubeutil.ListRadixDeployments(namespace) + radixDeployments, err := kubeutil.radixclient.RadixV1().RadixDeployments(namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { return nil, err } - for _, rd := range radixDeployments { + for _, rd := range radixDeployments.Items { if rd.Status.ActiveTo.IsZero() { - return rd, err + return &rd, err } } return nil, nil diff --git a/pkg/apis/pipeline/component_image.go b/pkg/apis/pipeline/component_image.go index 0696da46b..bc31aa4c3 100644 --- a/pkg/apis/pipeline/component_image.go +++ b/pkg/apis/pipeline/component_image.go @@ -1,12 +1,26 @@ package pipeline -// ComponentImage Holds info about the image associated with a component -type ComponentImage struct { +// DeployComponentImage Holds info about the image associated with a component +type DeployComponentImage struct { + ImagePath string + ImageTagName string + Build bool +} + +// DeployComponentImages maps component names with image information +type DeployComponentImages map[string]DeployComponentImage + +// DeployEnvironmentComponentImages maps environment names with components to be deployed +type DeployEnvironmentComponentImages map[string]DeployComponentImages + +// BuildComponentImage holds info about a build container +type BuildComponentImage struct { ContainerName string Context string Dockerfile string ImageName string ImagePath string - ImageTagName string - Build bool } + +// BuildComponentImages maps component names with build information +type BuildComponentImages map[string]BuildComponentImage diff --git a/pkg/apis/radix/v1/radixapptypes.go b/pkg/apis/radix/v1/radixapptypes.go index 831379c17..6654521ab 100644 --- a/pkg/apis/radix/v1/radixapptypes.go +++ b/pkg/apis/radix/v1/radixapptypes.go @@ -26,6 +26,34 @@ type RadixApplication struct { Spec RadixApplicationSpec `json:"spec"` } +// GetComponentByName returns the component matching the name parameter, or nil if not found +func (ra *RadixApplication) GetComponentByName(name string) *RadixComponent { + for _, comp := range ra.Spec.Components { + if comp.GetName() == name { + return &comp + } + } + return nil +} + +// GetJobComponentByName returns the job matching the name parameter, or nil if not found +func (ra *RadixApplication) GetJobComponentByName(name string) *RadixJobComponent { + for _, job := range ra.Spec.Jobs { + if job.GetName() == name { + return &job + } + } + return nil +} + +// GetCommonComponentByName returns the job or component matching the name parameter, or nil if not found +func (ra *RadixApplication) GetCommonComponentByName(name string) RadixCommonComponent { + if comp := ra.GetComponentByName(name); comp != nil { + return comp + } + return ra.GetJobComponentByName(name) +} + // RadixApplicationSpec is the specification for an application. type RadixApplicationSpec struct { // Build contains configuration used by pipeline jobs. @@ -1256,6 +1284,8 @@ type Notifications struct { type RadixCommonComponent interface { // GetName Gets component name GetName() string + // GetDockerfileName Gets component docker file name + GetDockerfileName() string // GetSourceFolder Gets component source folder GetSourceFolder() string // GetImage Gets component image @@ -1286,13 +1316,18 @@ type RadixCommonComponent interface { GetEnabledForEnv(RadixCommonEnvironmentConfig) bool // GetEnvironmentConfigByName Gets component environment configuration by its name GetEnvironmentConfigByName(environment string) RadixCommonEnvironmentConfig - GetEnabledForAnyEnvironment(environments []string) bool + // GetEnabledForEnvironment Checks if the component is enabled for any of the environments + GetEnabledForEnvironment(environment string) bool } func (component *RadixComponent) GetName() string { return component.Name } +func (component *RadixComponent) GetDockerfileName() string { + return component.DockerfileName +} + func (component *RadixComponent) GetSourceFolder() string { return component.SourceFolder } @@ -1366,8 +1401,8 @@ func (component *RadixComponent) GetEnabledForEnv(envConfig RadixCommonEnvironme return getEnabled(component, envConfig) } -func (component *RadixComponent) GetEnabledForAnyEnvironment(environments []string) bool { - return getEnabledForAnyEnvironment(component, environments) +func (component *RadixComponent) GetEnabledForEnvironment(environment string) bool { + return getEnabledForEnvironment(component, environment) } func (component *RadixJobComponent) GetEnabledForEnv(envConfig RadixCommonEnvironmentConfig) bool { @@ -1385,6 +1420,10 @@ func (component *RadixJobComponent) GetName() string { return component.Name } +func (component *RadixJobComponent) GetDockerfileName() string { + return component.DockerfileName +} + func (component *RadixJobComponent) GetSourceFolder() string { return component.SourceFolder } @@ -1459,8 +1498,8 @@ func (component *RadixJobComponent) GetEnvironmentConfigByName(environment strin return getEnvironmentConfigByName(environment, component.GetEnvironmentConfig()) } -func (component *RadixJobComponent) GetEnabledForAnyEnvironment(environments []string) bool { - return getEnabledForAnyEnvironment(component, environments) +func (component *RadixJobComponent) GetEnabledForEnvironment(environment string) bool { + return getEnabledForEnvironment(component, environment) } func getEnvironmentConfigByName(environment string, environmentConfigs []RadixCommonEnvironmentConfig) RadixCommonEnvironmentConfig { @@ -1472,15 +1511,10 @@ func getEnvironmentConfigByName(environment string, environmentConfigs []RadixCo return nil } -func getEnabledForAnyEnvironment(component RadixCommonComponent, environments []string) bool { +func getEnabledForEnvironment(component RadixCommonComponent, environment string) bool { environmentConfigsMap := component.GetEnvironmentConfigsMap() if len(environmentConfigsMap) == 0 { return component.getEnabled() } - for _, envName := range environments { - if component.GetEnabledForEnv(environmentConfigsMap[envName]) { - return true - } - } - return false + return component.GetEnabledForEnv(environmentConfigsMap[environment]) } diff --git a/pkg/apis/radix/v1/radixdeploytypes.go b/pkg/apis/radix/v1/radixdeploytypes.go index 6a851b18b..3482b7a6e 100644 --- a/pkg/apis/radix/v1/radixdeploytypes.go +++ b/pkg/apis/radix/v1/radixdeploytypes.go @@ -19,6 +19,7 @@ type RadixDeployment struct { Status RadixDeployStatus `json:"status" yaml:"status"` } +// GetComponentByName returns the component matching the name parameter, or nil if not found func (rd *RadixDeployment) GetComponentByName(name string) *RadixDeployComponent { for _, component := range rd.Spec.Components { if strings.EqualFold(component.Name, name) { @@ -28,6 +29,7 @@ func (rd *RadixDeployment) GetComponentByName(name string) *RadixDeployComponent return nil } +// GetJobComponentByName returns the job matching the name parameter, or nil if not found func (rd *RadixDeployment) GetJobComponentByName(name string) *RadixDeployJobComponent { for _, jobComponent := range rd.Spec.Jobs { if strings.EqualFold(jobComponent.Name, name) { @@ -37,6 +39,14 @@ func (rd *RadixDeployment) GetJobComponentByName(name string) *RadixDeployJobCom return nil } +// GetCommonComponentByName returns the component or job matching the name parameter, or nil if not found +func (rd *RadixDeployment) GetCommonComponentByName(name string) RadixCommonDeployComponent { + if comp := rd.GetComponentByName(name); comp != nil { + return comp + } + return rd.GetJobComponentByName(name) +} + // RadixDeployStatus is the status for a rd type RadixDeployStatus struct { ActiveFrom meta_v1.Time `json:"activeFrom" yaml:"activeFrom"` diff --git a/pkg/apis/radix/v1/radixenvironmentconfigtypes.go b/pkg/apis/radix/v1/radixenvironmentconfigtypes.go index 854ee0734..d9852db52 100644 --- a/pkg/apis/radix/v1/radixenvironmentconfigtypes.go +++ b/pkg/apis/radix/v1/radixenvironmentconfigtypes.go @@ -93,7 +93,7 @@ func (config RadixJobComponentEnvironmentConfig) GetIdentity() *Identity { return config.Identity } -//GetNotifications Get job component notifications +// GetNotifications Get job component notifications func (config RadixJobComponentEnvironmentConfig) GetNotifications() *Notifications { return config.Notifications } diff --git a/radix-operator/application/handler.go b/radix-operator/application/handler.go index 0ef6e529b..9a39ccb8b 100644 --- a/radix-operator/application/handler.go +++ b/radix-operator/application/handler.go @@ -78,11 +78,8 @@ func (t *Handler) Sync(namespace, name string, eventRecorder record.EventRecorde syncApplication := radixApplication.DeepCopy() logger.Debugf("Sync application %s", syncApplication.Name) - applicationConfig, err := application.NewApplicationConfig(t.kubeclient, t.kubeutil, t.radixclient, radixRegistration, radixApplication) - if err != nil { - // Put back on queue - return err - } + + applicationConfig := application.NewApplicationConfig(t.kubeclient, t.kubeutil, t.radixclient, radixRegistration, radixApplication) err = applicationConfig.OnSync() if err != nil { // Put back on queue