diff --git a/.github/workflows/pr-pantheon-tests.yml b/.github/workflows/pr-pantheon-tests.yml index 23ee382..1885852 100644 --- a/.github/workflows/pr-pantheon-tests.yml +++ b/.github/workflows/pr-pantheon-tests.yml @@ -28,6 +28,7 @@ jobs: - examples/pantheon-downstreamer-1 - examples/pantheon-downstreamer-2 - examples/pantheon-drush-uri + - examples/pantheon-php-warning - examples/wordpress - examples/wordpressnetworkdomain # - examples/wordpressnetworkfolder diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b7fb93..0486fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## {{ UNRELEASED_VERSION }} - [{{ UNRELEASED_DATE }}]({{ UNRELEASED_LINK }}) +* Fixed `manifest unknown` Docker error by validating `php_version` and `php_runtime_generation` against published images and falling back to a working generation when needed [#347](https://github.com/lando/pantheon/issues/347) + ## v1.13.0 - [March 11, 2026](https://github.com/lando/pantheon/releases/tag/v1.13.0) * Fixed `@@tx_isolation` usage for MariaDB 10.4 compatibility diff --git a/builders/pantheon-php.js b/builders/pantheon-php.js index adfcba2..99c8dc6 100644 --- a/builders/pantheon-php.js +++ b/builders/pantheon-php.js @@ -17,9 +17,17 @@ module.exports = { // rebase option on defaults options = _.merge({}, defaults, options); - // Normalize because 7.0/8.0 right away gets handled strangely by js-yaml - if (options.php === '7' || options.php === 7) options.php = '7.0'; - if (options.php === '8' || options.php === 8) options.php = '8.0'; + // Normalize because js-yaml parses unquoted x.0 PHP versions as the + // integer x (e.g. `php_version: 8.0` becomes the number 8). + options.php = String(options.php); + if (/^\d+$/.test(options.php)) options.php = `${options.php}.0`; + + // Safety check: if the php+generation combo has no published image, + // fall back to a generation that does. getPantheonConfig already does + // this with a user-facing warning; this is a silent second line of + // defense in case the builder is invoked without going through it. + const resolvedGen = utils.resolveGeneration(String(options.php), String(options.generation)); + if (resolvedGen !== null) options.generation = resolvedGen; // main event options.version = options.php; diff --git a/examples/pantheon-php-warning/.lando.yml b/examples/pantheon-php-warning/.lando.yml new file mode 100644 index 0000000..5927997 --- /dev/null +++ b/examples/pantheon-php-warning/.lando.yml @@ -0,0 +1,11 @@ +name: pantheon-php-warning +recipe: pantheon +config: + framework: drupal + site: fake-site + id: fake-id + +# do not remove this +plugins: + "@lando/pantheon": ../.. + "@lando/mariadb": ../../node_modules/@lando/mariadb diff --git a/examples/pantheon-php-warning/README.md b/examples/pantheon-php-warning/README.md new file mode 100644 index 0000000..c28082f --- /dev/null +++ b/examples/pantheon-php-warning/README.md @@ -0,0 +1,63 @@ +# Pantheon PHP Version Warning Example + +This example exists primarily to test the unsupported-PHP-version warning +introduced in [#347](https://github.com/lando/pantheon/pull/347). + +The bundled `pantheon.yml` sets `php_version: "6.0"` — a PHP version that +does not exist (the PHP 6 project was abandoned in 2010, and Pantheon has +never published a `6.0` appserver image). The Lando Pantheon plugin should +warn the user clearly during `lando start` instead of leaving them to +decode the cryptic Docker `manifest unknown` error that follows. + +`lando start` is *expected to fail* in this test — the appserver image +genuinely does not exist. We capture the start output to verify the +warning was emitted before that failure. + +## Start up tests + +Run the following commands to get up and running with this example. + +```bash +# Should poweroff +lando poweroff + +# Should attempt to start and emit a warning to the terminal. The actual +# `lando start` is expected to fail because devwithlando/pantheon-appserver:6.0-* +# does not exist, so we tolerate the non-zero exit code with `|| true` and +# rely on the verification step below to assert the warning fired first. +lando start 2>&1 | tee /tmp/pantheon-php-warning-start.log || true +``` + +## Verification commands + +Run the following commands to validate things are rolling as they should. + +```bash +# Should warn that no Docker images are available for PHP 6.0 +grep "WARNING: No Docker images are available for PHP 6.0" /tmp/pantheon-php-warning-start.log + +# Should explain that the appserver will fail to start +grep "appserver will fail to start" /tmp/pantheon-php-warning-start.log + +# Should advise updating php_version in the user's pantheon.yml +grep "Update php_version in your pantheon.yml" /tmp/pantheon-php-warning-start.log + +# Should recommend a Pantheon-supported PHP version +grep "Pantheon-recommended version" /tmp/pantheon-php-warning-start.log + +# Should NOT have a running appserver container (the 6.0 image doesn't exist) +docker ps --filter label=com.docker.compose.project=pantheonphpwarning --format '{{.Image}}' | grep "pantheon-appserver" || echo $? | grep 1 +``` + +## Destroy tests + +Run the following commands to trash this app like nothing ever happened. + +```bash +# Should be destroyed cleanly even though start failed +lando destroy -y || true +lando poweroff + +# Should clean up the start log +rm -f /tmp/pantheon-php-warning-start.log +``` diff --git a/examples/pantheon-php-warning/index.php b/examples/pantheon-php-warning/index.php new file mode 100644 index 0000000..480dcd5 --- /dev/null +++ b/examples/pantheon-php-warning/index.php @@ -0,0 +1 @@ +PHP FALLBACK diff --git a/examples/pantheon-php-warning/info.php b/examples/pantheon-php-warning/info.php new file mode 100644 index 0000000..147cebc --- /dev/null +++ b/examples/pantheon-php-warning/info.php @@ -0,0 +1 @@ + diff --git a/examples/pantheon-php-warning/pantheon.yml b/examples/pantheon-php-warning/pantheon.yml new file mode 100644 index 0000000..125faa4 --- /dev/null +++ b/examples/pantheon-php-warning/pantheon.yml @@ -0,0 +1,13 @@ +# This pantheon.yml exists to test the unsupported-PHP-version warning +# behavior introduced in https://github.com/lando/pantheon/pull/347. +# +# php_version: 6.0 is intentionally a PHP version that does not exist +# (PHP famously skipped 6.0; the project was abandoned in 2010) so this +# test stays valid even as Pantheon publishes new images. With no +# devwithlando/pantheon-appserver:6.0-* image available, the Lando +# Pantheon plugin should emit a clear, user-facing warning instead of +# leaving the user to decode a cryptic Docker "manifest unknown" error. +api_version: 1 +php_version: "6.0" +database: + version: 10.6 diff --git a/lib/utils.js b/lib/utils.js index d4755de..5a6817b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -26,6 +26,30 @@ const PANTHEON_CACHE_PASSWORD = 'pantheon'; const PANTHEON_EDGE_HTTP_RESP_HDR_LEN = '25k'; const PANTHEON_INDEX_HOST = 'index'; const PANTHEON_INDEX_SCHEME = 'http'; +// Default PHP version when none is set in pantheon.yml +const DEFAULT_PHP_VERSION = '8.3'; +// PHP versions Pantheon currently recommends for new sites. Update as Pantheon's +// support policy changes (https://docs.pantheon.io/guides/php/php-versions). +const RECOMMENDED_PHP_VERSIONS = ['8.2', '8.3', '8.4']; +// Maps each PHP version to the list of pantheon-appserver image generations +// available on Docker Hub (devwithlando/pantheon-appserver:-). +// Used to validate php_version + php_runtime_generation combinations and fall +// back to the closest working image when the requested combo has no image. +const PHP_GENERATION_IMAGES = { + '5.3': ['2'], + '5.5': ['2'], + '5.6': ['2', '3', '4'], + '7.0': ['2', '3', '4'], + '7.1': ['2', '3', '4'], + '7.2': ['2', '3', '4', '5'], + '7.3': ['2', '3', '4', '5'], + '7.4': ['2', '3', '4', '5'], + '8.0': ['3', '4', '5'], + '8.1': ['4', '5'], + '8.2': ['4', '5'], + '8.3': ['4', '5'], + '8.4': ['5'], +}; const PATH = [ '/app/vendor/bin', '/usr/local/sbin', @@ -209,6 +233,22 @@ exports.getPantheonCache = { }, }; +/** + * Resolves a (PHP version, image generation) pair to a generation that has an + * image available on Docker Hub. Returns the requested generation if it exists, + * otherwise the highest available generation for that PHP version, or null if + * no images are available at all. + * @param {string} php - PHP version (e.g. '8.3') + * @param {string} generation - Requested image generation (e.g. '5') + * @return {string|null} Generation to use, or null if no images exist + */ +const resolveGeneration = (php, generation) => { + const available = PHP_GENERATION_IMAGES[php]; + if (!available || available.length === 0) return null; + if (available.includes(generation)) return generation; + return available[available.length - 1]; +}; + /** * Merges and processes Pantheon YAML configuration files * @param {string[]} [files=['pantheon.upstream.yml', 'pantheon.yml']] - YAML files to process @@ -220,7 +260,10 @@ exports.getPantheonConfig = (files = ['pantheon.upstream.yml', 'pantheon.yml']) .thru(data => _.merge({}, ...data)) .thru(data => { // Set the php version - data.php = _.toString(_.get(data, 'php_version', '8.3')); + data.php = _.toString(_.get(data, 'php_version', DEFAULT_PHP_VERSION)); + // Normalize because js-yaml parses unquoted x.0 versions as the integer x + // (e.g. `php_version: 8.0` becomes the number 8 since 8.0 === 8 in JS). + if (/^\d+$/.test(data.php)) data.php = `${data.php}.0`; // Set the webroot data.webroot = (_.get(data, 'web_docroot', false)) ? 'web' : '.'; // Set the drush version @@ -229,7 +272,31 @@ exports.getPantheonConfig = (files = ['pantheon.upstream.yml', 'pantheon.yml']) if (data.drush < 8) data.drush = 8; // @DEPRECATED: Pantheon php_runtime_generation: 1 is deprecated and will be removed April 2026. const phpRuntimeGen = _.get(data, 'php_runtime_generation', 2); - data.generation = phpRuntimeGen === 1 ? '4' : '5'; + const requestedGen = phpRuntimeGen === 1 ? '4' : '5'; + // Validate the (php, generation) combo has a published image, falling back + // when possible. This prevents the cryptic "manifest unknown" Docker error. + const resolvedGen = resolveGeneration(data.php, requestedGen); + if (resolvedGen === null) { + console.warn([ + ``, + `⚠️ WARNING: No Docker images are available for PHP ${data.php}.`, + ` The appserver will fail to start. Update php_version in your pantheon.yml`, + ` to a Pantheon-recommended version: ${RECOMMENDED_PHP_VERSIONS.join(', ')}.`, + ``, + ].join('\n')); + data.generation = requestedGen; + } else if (resolvedGen !== requestedGen) { + console.warn([ + ``, + `⚠️ WARNING: No Docker image exists for PHP ${data.php} generation ${requestedGen}.`, + ` Falling back to devwithlando/pantheon-appserver:${data.php}-${resolvedGen}.`, + ` For best results, use a Pantheon-recommended PHP version: ${RECOMMENDED_PHP_VERSIONS.join(', ')}.`, + ``, + ].join('\n')); + data.generation = resolvedGen; + } else { + data.generation = resolvedGen; + } // Set the tika version if specified in pantheon.yml const tikaVersion = _.get(data, 'tika_version'); if (tikaVersion !== undefined) { @@ -240,6 +307,11 @@ exports.getPantheonConfig = (files = ['pantheon.upstream.yml', 'pantheon.yml']) }) .value(); +// Exported so the builder can reuse the same data and avoid drift +exports.PHP_GENERATION_IMAGES = PHP_GENERATION_IMAGES; +exports.RECOMMENDED_PHP_VERSIONS = RECOMMENDED_PHP_VERSIONS; +exports.resolveGeneration = resolveGeneration; + /** * Builds configuration for Pantheon Varnish edge service * @param {Object} options - Configuration options diff --git a/test/pantheon-php.spec.js b/test/pantheon-php.spec.js new file mode 100644 index 0000000..2ebe382 --- /dev/null +++ b/test/pantheon-php.spec.js @@ -0,0 +1,53 @@ +'use strict'; + +const chai = require('chai'); +const pantheonPhp = require('../builders/pantheon-php'); + +chai.should(); + +describe('pantheon-php', () => { + it('should silently fall back to an available image generation', () => { + class Parent { + constructor(id, options) { + this.options = options; + } + } + + class PantheonNginx { + constructor() { + this.data = [{version: '3'}]; + this.info = {}; + } + } + + const app = { + add: () => {}, + config: {services: {}}, + env: {LANDO_HOST_IP: '127.0.0.1'}, + info: [], + _lando: {log: {debug: () => {}}}, + }; + const factory = { + get: () => PantheonNginx, + }; + const PantheonPhp = pantheonPhp.builder(Parent, pantheonPhp.defaults); + const service = new PantheonPhp('appserver', { + _app: app, + app: 'pantheon', + confDest: '/tmp/lando/config', + framework: 'drupal', + generation: '4', + id: 'site-id', + name: 'appserver', + php: '8.4', + project: 'pantheon', + root: '/app', + site: 'site-name', + solrTag: 'latest', + userConfRoot: '/app/.lando', + volumes: [], + }, factory); + + service.options.image.should.equal('devwithlando/pantheon-appserver:8.4-5'); + }); +}); diff --git a/test/utils.spec.js b/test/utils.spec.js new file mode 100644 index 0000000..38e183f --- /dev/null +++ b/test/utils.spec.js @@ -0,0 +1,84 @@ +'use strict'; + +const chai = require('chai'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const utils = require('../lib/utils'); + +chai.should(); + +describe('utils', () => { + describe('#resolveGeneration', () => { + it('should fall back to the highest available generation', () => { + utils.resolveGeneration('8.4', '4').should.equal('5'); + }); + + it('should return null when no PHP generation images exist', () => { + chai.expect(utils.resolveGeneration('9.9', '5')).to.equal(null); + }); + }); + + describe('#getPantheonConfig', () => { + let tempDir; + let originalWarn; + let warnings; + + const writePantheonConfig = content => { + const file = path.join(tempDir, 'pantheon.yml'); + fs.writeFileSync(file, content); + return file; + }; + + beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lando-pantheon-')); + warnings = []; + originalWarn = console.warn; + console.warn = warning => warnings.push(warning); + }); + + afterEach(() => { + console.warn = originalWarn; + fs.rmSync(tempDir, {recursive: true, force: true}); + }); + + it('should normalize unquoted x.0 PHP versions before resolving the generation', () => { + const file = writePantheonConfig([ + 'php_version: 8.0', + 'php_runtime_generation: 1', + ].join('\n')); + + const config = utils.getPantheonConfig([file]); + + config.php.should.equal('8.0'); + config.generation.should.equal('4'); + }); + + it('should warn and fall back when the requested PHP generation has no image', () => { + const file = writePantheonConfig([ + 'php_version: 8.4', + 'php_runtime_generation: 1', + ].join('\n')); + + const config = utils.getPantheonConfig([file]); + + config.generation.should.equal('5'); + warnings.should.have.length(1); + warnings[0].should.include('No Docker image exists for PHP 8.4 generation 4'); + warnings[0].should.include('devwithlando/pantheon-appserver:8.4-5'); + }); + + it('should warn and keep the requested generation when no PHP images exist', () => { + const file = writePantheonConfig([ + 'php_version: 9.9', + 'php_runtime_generation: 2', + ].join('\n')); + + const config = utils.getPantheonConfig([file]); + + config.generation.should.equal('5'); + warnings.should.have.length(1); + warnings[0].should.include('No Docker images are available for PHP 9.9'); + }); + }); +});