Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr-pantheon-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 11 additions & 3 deletions builders/pantheon-php.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions examples/pantheon-php-warning/.lando.yml
Original file line number Diff line number Diff line change
@@ -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
63 changes: 63 additions & 0 deletions examples/pantheon-php-warning/README.md
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions examples/pantheon-php-warning/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PHP FALLBACK
1 change: 1 addition & 0 deletions examples/pantheon-php-warning/info.php
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php phpinfo(); ?>
13 changes: 13 additions & 0 deletions examples/pantheon-php-warning/pantheon.yml
Original file line number Diff line number Diff line change
@@ -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
76 changes: 74 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:<version>-<gen>).
// 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',
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
}
Comment thread
cursor[bot] marked this conversation as resolved.
// Set the tika version if specified in pantheon.yml
const tikaVersion = _.get(data, 'tika_version');
if (tikaVersion !== undefined) {
Expand All @@ -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
Expand Down
53 changes: 53 additions & 0 deletions test/pantheon-php.spec.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
84 changes: 84 additions & 0 deletions test/utils.spec.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
Loading