Skip to content

Commit

Permalink
feat(manager): Cloud Native Buildpacks project descriptor manager (#3…
Browse files Browse the repository at this point in the history
…0799)

Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: loewenstein-sap <jan.von.loewenstein@sap.com>
  • Loading branch information
4 people authored Nov 21, 2024
1 parent 6bdd27a commit bbedb2d
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,7 @@ const options: RenovateOptions[] = [
supportedManagers: [
'ansible',
'bitbucket-pipelines',
'buildpacks',
'crossplane',
'devcontainer',
'docker-compose',
Expand Down
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as bicep from './bicep';
import * as bitbucketPipelines from './bitbucket-pipelines';
import * as bitrise from './bitrise';
import * as buildkite from './buildkite';
import * as buildpacks from './buildpacks';
import * as bun from './bun';
import * as bunVersion from './bun-version';
import * as bundler from './bundler';
Expand Down Expand Up @@ -117,6 +118,7 @@ api.set('bicep', bicep);
api.set('bitbucket-pipelines', bitbucketPipelines);
api.set('bitrise', bitrise);
api.set('buildkite', buildkite);
api.set('buildpacks', buildpacks);
api.set('bun', bun);
api.set('bun-version', bunVersion);
api.set('bundler', bundler);
Expand Down
111 changes: 111 additions & 0 deletions lib/modules/manager/buildpacks/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { codeBlock } from 'common-tags';

import { extractPackageFile } from '.';

describe('modules/manager/buildpacks/extract', () => {
describe('extractPackageFile()', () => {
it('returns null for invalid files', () => {
expect(extractPackageFile('not a project toml', '', {})).toBeNull();
});

it('returns null for empty package.toml', () => {
const res = extractPackageFile(
'[_]\nschema-version = "0.2"',
'project.toml',
{},
);
expect(res).toBeNull();
});

it('extracts builder and buildpack images', () => {
const res = extractPackageFile(
codeBlock`
[_]
schema-version = "0.2"
[io.buildpacks]
builder = "registry.corp/builder/noble:1.1.1"
[[io.buildpacks.group]]
uri = "docker://buildpacks/java:2.2.2"
[[io.buildpacks.group]]
uri = "buildpacks/nodejs:3.3.3"
[[io.buildpacks.group]]
uri = "example/foo@1.0.0"
[[io.buildpacks.group]]
uri = "example/registry-cnb"
[[io.buildpacks.group]]
uri = "urn:cnb:registry:example/foo@1.0.0"
[[io.buildpacks.group]]
uri = "some-bp@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[[io.buildpacks.group]]
uri = "cnbs/some-bp:some-tag@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[[io.buildpacks.group]]
uri = "from=builder:foobar"
[[io.buildpacks.group]]
uri = "file://local.oci"
[[io.buildpacks.group]]
uri = "foo://fake.oci"`,
'project.toml',
{},
);
expect(res?.deps).toEqual([
{
autoReplaceStringTemplate:
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
commitMessageTopic: 'builder {{depName}}',
currentValue: '1.1.1',
datasource: 'docker',
depName: 'registry.corp/builder/noble',
replaceString: 'registry.corp/builder/noble:1.1.1',
},
{
autoReplaceStringTemplate:
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
currentValue: '2.2.2',
datasource: 'docker',
depName: 'buildpacks/java',
replaceString: 'buildpacks/java:2.2.2',
},
{
autoReplaceStringTemplate:
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
currentValue: '3.3.3',
datasource: 'docker',
depName: 'buildpacks/nodejs',
replaceString: 'buildpacks/nodejs:3.3.3',
},
{
autoReplaceStringTemplate:
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
currentDigest:
'sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
datasource: 'docker',
depName: 'some-bp',
replaceString:
'some-bp@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
},
{
autoReplaceStringTemplate:
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
currentDigest:
'sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
currentValue: 'some-tag',
datasource: 'docker',
depName: 'cnbs/some-bp',
replaceString:
'cnbs/some-bp:some-tag@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
},
]);
});
});
});
103 changes: 103 additions & 0 deletions lib/modules/manager/buildpacks/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { regEx } from '../../../util/regex';
import { getDep } from '../dockerfile/extract';
import type {
ExtractConfig,
PackageDependency,
PackageFileContent,
} from '../types';
import { type ProjectDescriptor, ProjectDescriptorToml } from './schema';

const dockerPrefix = regEx(/^docker:\/?\//);
const dockerRef = regEx(
/^((?:[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?)*)(?::\d{2,5}\/)?)?[a-z\d]+((\.|_|__|-+)[a-z\d]+)*(\/[a-z\d]+((\.|_|__|-+)[a-z\d]+)*)*(?::(\w[\w.-]{0,127})(?:@sha256:[A-Fa-f\d]{32,})?|@sha256:[A-Fa-f\d]{32,})$/,
);

function isDockerRef(ref: string): boolean {
if (ref.startsWith('docker:/') || dockerRef.test(ref)) {
return true;
}
return false;
}

function parseProjectToml(
content: string,
packageFile: string,
): ProjectDescriptor | null {
const res = ProjectDescriptorToml.safeParse(content);
if (res.success) {
return res.data;
}

logger.debug(
{ packageFile, err: res.error },
'Failed to parse buildpacks project descriptor TOML',
);

return null;
}

export function extractPackageFile(
content: string,
packageFile: string,
config: ExtractConfig,
): PackageFileContent | null {
const deps: PackageDependency[] = [];

const descriptor = parseProjectToml(content, packageFile);
if (!descriptor) {
return null;
}

if (
descriptor.io?.buildpacks?.builder &&
isDockerRef(descriptor.io.buildpacks.builder)
) {
const dep = getDep(
descriptor.io.buildpacks.builder.replace(dockerPrefix, ''),
true,
config.registryAliases,
);
logger.trace(
{
depName: dep.depName,
currentValue: dep.currentValue,
currentDigest: dep.currentDigest,
},
'Cloud Native Buildpacks builder',
);

deps.push({ ...dep, commitMessageTopic: 'builder {{depName}}' });
}

if (
descriptor.io?.buildpacks?.group &&
is.array(descriptor.io.buildpacks.group)
) {
for (const group of descriptor.io.buildpacks.group) {
if (group.uri && isDockerRef(group.uri)) {
const dep = getDep(
group.uri.replace(dockerPrefix, ''),
true,
config.registryAliases,
);
logger.trace(
{
depName: dep.depName,
currentValue: dep.currentValue,
currentDigest: dep.currentDigest,
},
'Cloud Native Buildpack',
);

deps.push(dep);
}
}
}

if (!deps.length) {
return null;
}
return { deps };
}
12 changes: 12 additions & 0 deletions lib/modules/manager/buildpacks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Category } from '../../../constants';
import { DockerDatasource } from '../../datasource/docker';
export { extractPackageFile } from './extract';

export const defaultConfig = {
commitMessageTopic: 'buildpack {{depName}}',
fileMatch: ['(^|/)project\\.toml$'],
pinDigests: false,
};

export const categories: Category[] = ['docker'];
export const supportedDatasources = [DockerDatasource.id];
26 changes: 26 additions & 0 deletions lib/modules/manager/buildpacks/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
The `buildpacks` manager updates Cloud Native Buildpacks project descriptors in `project.toml` files.
A `project.toml` file can reference builder / buildpack images by URIs.
Renovate can update a `project.toml` file if:

- It can find the file
- The file follows the [project descriptor specifications](https://github.com/buildpacks/spec/blob/main/extensions/project-descriptor.md)
- The buildpack `uri` is an OCI image reference (references to a local file or buildpack registry are ignored)

If you use buildpacks in the `io.buildpacks.group` array, then you _must_ configure the Docker reference (`uri`) for Renovate to work.

```toml title="Example of a project.toml file with Docker reference URIs"
[_]
schema-version = "0.2"

[io.buildpacks]
builder = "registry.corp/builder/noble:1.1.1"

[[io.buildpacks.group]]
uri = "docker://buildpacks/java:2.2.2"

[[io.buildpacks.group]]
uri = "buildpacks/nodejs:3.3.3"

[[io.buildpacks.group]]
uri = "file://local.oci" # will be ignored
```
25 changes: 25 additions & 0 deletions lib/modules/manager/buildpacks/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from 'zod';
import { Toml } from '../../../util/schema-utils';

const BuildpackGroup = z.object({
uri: z.string().optional(),
});

const IoBuildpacks = z.object({
builder: z.string().optional(),
group: z.array(BuildpackGroup).optional(),
});

export const ProjectDescriptor = z.object({
_: z.object({
'schema-version': z.string(),
}),
io: z
.object({
buildpacks: IoBuildpacks.optional(),
})
.optional(),
});

export type ProjectDescriptor = z.infer<typeof ProjectDescriptor>;
export const ProjectDescriptorToml = Toml.pipe(ProjectDescriptor);

0 comments on commit bbedb2d

Please sign in to comment.