diff --git a/linters/cf_manifest.go b/linters/cf_manifest.go index 1ef3a8b5..1d310dbc 100644 --- a/linters/cf_manifest.go +++ b/linters/cf_manifest.go @@ -1,8 +1,10 @@ package linters import ( + "fmt" "github.com/springernature/halfpipe/cf" "github.com/springernature/halfpipe/config" + "github.com/springernature/halfpipe/renderers/shared/secrets" "golang.org/x/exp/slices" "strings" @@ -18,8 +20,8 @@ func LintCfManifest(task manifest.DeployCF, readCfManifest cf.ManifestReader) (e return errs } - manifest, err := readCfManifest(task.Manifest, nil, nil) - apps := manifest.Applications + cfManifest, err := readCfManifest(task.Manifest, nil, nil) + apps := cfManifest.Applications if err != nil { errs = append(errs, ErrFileInvalid.WithValue(err.Error()).WithFile(task.Manifest)) @@ -37,12 +39,28 @@ func LintCfManifest(task manifest.DeployCF, readCfManifest cf.ManifestReader) (e } errs = append(errs, lintRoutes(task, app)...) + errs = append(errs, lintCandidateAppRoute(task, cfManifest)...) errs = append(errs, lintDockerPush(task, app)...) errs = append(errs, lintBuildpack(app, task.Manifest)...) return errs } +func lintCandidateAppRoute(task manifest.DeployCF, m manifestparser.Manifest) (errs []error) { + if secrets.IsSecret(task.Space) { + return errs + } + + testRouteHost := fmt.Sprintf("%s-%s-CANDIDATE", m.GetFirstApp().Name, task.Space) + + if len(testRouteHost) > 64 { + errs = append(errs, ErrCFCandidateRouteTooLong.WithValue(fmt.Sprintf("%s length is %v", testRouteHost, len(testRouteHost)))) + return + } + + return errs +} + func lintDockerPush(task manifest.DeployCF, app manifestparser.Application) (errs []error) { if app.Docker != nil { if task.DeployArtifact != "" { diff --git a/linters/cf_manifest_test.go b/linters/cf_manifest_test.go index d6f3d9c4..fdeadc6a 100644 --- a/linters/cf_manifest_test.go +++ b/linters/cf_manifest_test.go @@ -212,4 +212,38 @@ applications: assert.Empty(t, errs) }) + t.Run("candidate app route is too long", func(t *testing.T) { + cfManifest := ` +applications: +- name: test-a-veeeeeeeeeeeeeeeeeeeery-loooooooooooooooong-app + routes: + - route: test.com + buildpacks: + - java +` + errs := LintCfManifest(manifest.DeployCF{ + Space: "with-a-very-loooong-space", + TestDomain: "", + }, cfManifestReader(cfManifest, nil)) + + assertContainsError(t, errs, ErrCFCandidateRouteTooLong) + }) + + t.Run("candidate app route linting is ignored when space is secret", func(t *testing.T) { + cfManifest := ` +applications: +- name: test-a-veeeeeeeeeeeeeeeeeeeery-loooooooooooooooong-app + routes: + - route: test.com + buildpacks: + - java +` + errs := LintCfManifest(manifest.DeployCF{ + Space: "((halfpipe.test))", + TestDomain: "", + }, cfManifestReader(cfManifest, nil)) + + assertNotContainsError(t, errs, ErrCFCandidateRouteTooLong) + }) + } diff --git a/linters/errors.go b/linters/errors.go index 95ebd478..3f9fef28 100644 --- a/linters/errors.go +++ b/linters/errors.go @@ -19,19 +19,20 @@ var ( ErrFileNotExecutable = newError("file is not executable") ErrFileInvalid = newError("file is invalid") - ErrCFMissingRoutes = newError("cf application must have at least one route") - ErrCFMissingName = newError("cf application missing 'name'") - ErrCFRoutesAndNoRoute = newError("cf application cannot have both 'routes' and 'no-route'") - ErrCFNoRouteHealthcheck = newError("cf application with 'no-route: true' requires 'health-check-type: process'") - ErrCFRouteScheme = newError("cf application route must not start with http(s)://") - ErrCFRouteMissing = newError("cf application routes must contain sso_route") - ErrCFMultipleApps = newError("cf manifest must have exactly 1 application") - ErrCFBuildpackUnversioned = newError("buildpack specified without version so the latest will be used on each deploy") - ErrCFBuildpackMissing = newError("buildpack missing. Cloud Foundry will try to detect which system buildpack to use. Please see ") - ErrCFBuildpackDeprecated = newError("'buildpack' is deprecated in favour of 'buildpacks'. Please see ") - ErrCFArtifactAndDocker = newError("cannot combine 'deploy_artifact' in the halfpipe task and 'docker' in the cf manifest") - ErrCFFromArtifact = newError("this file must be saved as an artifact in a previous task") - ErrCFPrePromoteArtifact = newError("cannot have pre promote tasks with CF manifest restored from artifact") + ErrCFMissingRoutes = newError("cf application must have at least one route") + ErrCFMissingName = newError("cf application missing 'name'") + ErrCFRoutesAndNoRoute = newError("cf application cannot have both 'routes' and 'no-route'") + ErrCFNoRouteHealthcheck = newError("cf application with 'no-route: true' requires 'health-check-type: process'") + ErrCFRouteScheme = newError("cf application route must not start with http(s)://") + ErrCFRouteMissing = newError("cf application routes must contain sso_route") + ErrCFMultipleApps = newError("cf manifest must have exactly 1 application") + ErrCFBuildpackUnversioned = newError("buildpack specified without version so the latest will be used on each deploy") + ErrCFBuildpackMissing = newError("buildpack missing. Cloud Foundry will try to detect which system buildpack to use. Please see ") + ErrCFBuildpackDeprecated = newError("'buildpack' is deprecated in favour of 'buildpacks'. Please see ") + ErrCFArtifactAndDocker = newError("cannot combine 'deploy_artifact' in the halfpipe task and 'docker' in the cf manifest") + ErrCFFromArtifact = newError("this file must be saved as an artifact in a previous task") + ErrCFPrePromoteArtifact = newError("cannot have pre promote tasks with CF manifest restored from artifact") + ErrCFCandidateRouteTooLong = newError("cf does not allow routes of more than 64 characters") ErrUnsupportedRegistry = newError("image must be from halfpipe registry. Please see ") ErrDockerPushTag = newError("the field 'tag' is no longer used and is safe to delete") diff --git a/renderers/shared/secrets/secrets.go b/renderers/shared/secrets/secrets.go index ed80b01c..64984fd8 100644 --- a/renderers/shared/secrets/secrets.go +++ b/renderers/shared/secrets/secrets.go @@ -15,7 +15,7 @@ type Secret struct { // New returns a Secret from a string in the "halfpipe" format // "((map.key))" or "((/path/to/map key))" func New(s string, team string) *Secret { - if !isSecret(s) { + if !IsSecret(s) { return nil } @@ -45,7 +45,7 @@ func New(s string, team string) *Secret { return nil } -func isSecret(s string) bool { +func IsSecret(s string) bool { return strings.HasPrefix(s, "((") && strings.HasSuffix(s, "))") }