Skip to content

Commit

Permalink
feat(core/cli) add docker release stage with sensible default (#81)
Browse files Browse the repository at this point in the history
* feat(core/cli) add docker release stage with sensible default

Following the discussion #34,
this PR implements a release stack for docker images that can be built in the repository and
with that adds a sensitive default to the Pipelinit configuration file with the possibility to other 'registries' for the generated CI.

The template basically looks for paths with Dockerfiles that can be built and
adds metadata involving the path found and the specified version.

Resolves: #80

* feat: add docker release stage e2e test
  • Loading branch information
joao10lima authored Mar 7, 2022
1 parent df4ba1d commit a2fc245
Show file tree
Hide file tree
Showing 30 changed files with 659 additions and 8 deletions.
19 changes: 18 additions & 1 deletion cli/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,22 @@ import {
} from "../../deps.ts";
import { arePlatforms, isPlatform, Platforms } from "./platform.ts";

export interface StackRegistry {
docker: string[];
}

const sensibleDefault = `
# Registries to publish artifacts.
# [registries]
# Default values per stack are presented below.
# If your project use other registry(ies), uncomment and edit
# docker = ["registry.hub.docker.com"]
`;

export const CONFIG_FILE = ".pipelinit.toml";
export type Config = {
platforms?: Platforms;
registries?: StackRegistry;
exists: () => Promise<boolean>;
load: () => Promise<void>;
save: () => Promise<void>;
Expand All @@ -31,14 +44,18 @@ export function isConfig(c: Record<string, unknown>): c is Config {
export const config: Config = {
exists: async () => await fileExists(CONFIG_FILE),
save: async () => {
await Deno.writeTextFile(CONFIG_FILE, stringifyToml(config));
await Deno.writeTextFile(
CONFIG_FILE,
stringifyToml(config) + sensibleDefault,
);
},
load: async () => {
const configContent = parseToml(await Deno.readTextFile(CONFIG_FILE));
// FIXME this validation is weak
if (isConfig(configContent)) {
const newConfig = deepMerge(config, configContent);
config.platforms = newConfig.platforms;
config.registries = newConfig.registries;
} else {
throw new Error("Couldn't parse configuration file");
}
Expand Down
7 changes: 7 additions & 0 deletions cli/src/lib/context/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,10 @@ export async function* readLines(path: string): AsyncIterableIterator<string> {
export async function readText(path: string): Promise<string> {
return await Deno.readTextFile(path);
}

/**
* Return a string representing the current working directory
*/
export async function filesWorkDir(): Promise<string> {
return await Deno.cwd();
}
2 changes: 2 additions & 0 deletions cli/src/lib/context/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Context, log, PIPELINIT_VERSION, semver } from "../../../deps.ts";
import { GlobalOptions } from "../../options.ts";
import {
each,
filesWorkDir,
includes,
readJSON,
readLines,
Expand All @@ -13,6 +14,7 @@ export { anyError, outputErrors } from "./errors.ts";

export const context: Context = {
getLogger: log.getLogger,
filesWorkDir: filesWorkDir,
files: {
each,
includes,
Expand Down
17 changes: 17 additions & 0 deletions core/plugins/stack/docker/contextPaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IntrospectFn } from "../../../types.ts";

export interface DockerContext {
paths: Set<string>;
}

export const introspect: IntrospectFn<DockerContext> = async (context) => {
const dockerContextPaths = new Set();
for await (const file of context.files.each("**/Dockerfile")) {
const contextPath = file.path.replace(
await context.filesWorkDir() + "/",
"",
).replace("/" + file.name, "").replace(file.name, "");
dockerContextPaths.add(contextPath);
}
return <DockerContext> { paths: dockerContextPaths };
};
73 changes: 73 additions & 0 deletions core/plugins/stack/docker/mod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { context } from "../../../tests/mod.ts";
import { assertEquals, deepMerge } from "../../../deps.ts";
import { config, StackRegistry } from "../../../../cli/src/lib/config.ts";
import { Platforms } from "../../../../cli/src/lib/platform.ts";
import { FileEntry } from "../../../types.ts";

import { introspector } from "./mod.ts";

config.platforms = <Platforms> ["github"];

const fakeContext = () => {
return deepMerge(
context,
{
files: {
// deno-lint-ignore require-await
includes: async (glob: string): Promise<boolean> => {
if (glob === "**/Dockerfile") {
return true;
}
return false;
},
each: async function* (glob: string): AsyncIterableIterator<FileEntry> {
if (glob === "**/Dockerfile") {
yield {
name: "Dockerfile",
path: "fake-path",
};
}
return;
},
},
},
);
};

Deno.test("Plugins > Check if Dockerfile is identified", async () => {
const result = await introspector.introspect(
fakeContext(),
);

assertEquals(result, {
dockerContext: {
paths: new Set(["fake-path"]),
},
hasDockerImage: true,
registries: {
urls: [
"registry.hub.docker.com",
],
},
});
});

Deno.test("Plugins > Check if Dockerfile is identified and Other registry", async () => {
config.registries = <StackRegistry> { docker: ["ghcr.io"] };

const result = await introspector.introspect(
fakeContext(),
);

assertEquals(result, {
dockerContext: {
paths: new Set(["fake-path"]),
},
hasDockerImage: true,
registries: {
urls: [
"ghcr.io",
],
},
});
});
24 changes: 23 additions & 1 deletion core/plugins/stack/docker/mod.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { Introspector } from "../../../types.ts";
import { introspect as introspectRegistries, Registries } from "./registry.ts";
import {
DockerContext,
introspect as introspectDockerContext,
} from "./contextPaths.ts";

/**
* Introspected information about a project with Docker
*/
export default interface DockerProject {
hasDockerImage: true;
registries: Registries;
dockerContext: DockerContext;
}

const registryWarn = `
Creating a release Pipelinit. Make sure to create the following secrets generated on the workflow:
REGISTRY_USERNAME -> Registry username
REGISTRY_PASSWORD -> Registry password
REGISTRY_ORGANIZATION -> Registry project organization
`;

export const introspector: Introspector<DockerProject> = {
detect: async (context) => {
return await context.files.includes("**/Dockerfile");
Expand All @@ -15,6 +29,14 @@ export const introspector: Introspector<DockerProject> = {
const logger = await context.getLogger("docker");
logger.debug("detected docker image");

return { hasDockerImage: true };
const registries = await introspectRegistries(context);
const dockerContext = await introspectDockerContext(context);
logger.info(registryWarn);

return {
hasDockerImage: true,
registries: registries,
dockerContext: dockerContext,
};
},
};
14 changes: 14 additions & 0 deletions core/plugins/stack/docker/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IntrospectFn } from "../../../types.ts";
import { config } from "../../../../cli/src/lib/config.ts";

export interface Registries {
urls: string[];
}

export const introspect: IntrospectFn<Registries> = async () => {
const configRegistry = await config.registries?.docker;
if (configRegistry) {
return <Registries> { urls: configRegistry };
}
return <Registries> { urls: ["registry.hub.docker.com"] };
};
50 changes: 50 additions & 0 deletions core/templates/github/docker/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Release new version

on:
push:
tags:
- "*.*.*"

env:
GITHUB_REPOSITORY: ${{ github.repository }}

jobs:
release:
name: Build and publish new release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set current version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set repository name
run: echo "GITHUB_REPOSITORY=${GITHUB_REPOSITORY/*\/}" >> $GITHUB_ENV

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
<% it.registries.urls.forEach( function(url) { %>
- name: Log in to the registry <%= url %>
uses: docker/login-action@v1
with:
registry: <%= url %>
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
<% it.dockerContext.paths.forEach( function(path) { %>
<%_ let dockerfilePath = !path ? "" : "-" + path -%>
- name: Extract metadata (tags, labels) <%= path %>
id: meta<%= dockerfilePath %>
uses: docker/metadata-action@v3
with:
tags: |
type=semver,pattern={{version}},enable=true
images: <%= url %>/${{ secrets.REGISTRY_ORGANIZATION }}/${{ env.GITHUB_REPOSITORY }}<%= dockerfilePath %>

- name: Build and push Docker image <%= path %>
uses: docker/build-push-action@v2
with:
build-args: "version=${{ env.RELEASE_VERSION }}"
context: ./<%= path %>
push: true
tags: ${{ steps.meta<%= dockerfilePath %>.outputs.tags }}
labels: ${{ steps.meta<%= dockerfilePath %>.outputs.labels }}
<% }) %>
<% }) %>
6 changes: 3 additions & 3 deletions core/templates/github/docker/sast.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: returntocorp/semgrep-action@v1
with:
config: >-
p/docker
with:
config: >-
p/docker
4 changes: 4 additions & 0 deletions core/tests/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export const context: Context = {
critical(_: string) {},
};
},
// deno-lint-ignore require-await
async filesWorkDir() {
return "";
},
files: {
// deno-lint-ignore require-yield
async *each(_) {
Expand Down
1 change: 1 addition & 0 deletions core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface SemVerHelpers {

export type Context = {
getLogger(name?: string): Logger;
filesWorkDir(): Promise<string>;
files: {
each(glob: string): AsyncIterableIterator<FileEntry>;
includes(glob: string): Promise<boolean>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated with pipelinit 0.4.0
# https://pipelinit.com/
name: Release new version

on:
push:
tags:
- "*.*.*"

env:
GITHUB_REPOSITORY: ${{ github.repository }}

jobs:
release:
name: Build and publish new release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set current version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Set repository name
run: echo "GITHUB_REPOSITORY=${GITHUB_REPOSITORY/*\/}" >> $GITHUB_ENV

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Log in to the registry registry.hub.docker.com
uses: docker/login-action@v1
with:
registry: registry.hub.docker.com
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v3
with:
tags: |
type=semver,pattern={{version}},enable=true
images: registry.hub.docker.com/${{ secrets.REGISTRY_ORGANIZATION }}/${{ env.GITHUB_REPOSITORY }}

- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
build-args: "version=${{ env.RELEASE_VERSION }}"
context: ./
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: returntocorp/semgrep-action@v1
with:
config: >-
p/docker
with:
config: >-
p/docker
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated with pipelinit 0.4.0
# https://pipelinit.com/
name: Build Docker
on:
pull_request:
paths:
- "**Dockerfile"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build Dockerfiles
run: |
for dockerfile in $(find . -iname "*Dockerfile*" -o -iwholename "./Dockerfile"); do
echo "Starting build for the Dockerfile $dockerfile"
docker build . --file $dockerfile
done
shell: bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Generated with pipelinit 0.4.0
# https://pipelinit.com/
name: Docker file Lint
on:
pull_request:
paths:
- "**Dockerfile"
jobs:
lint:
runs-on: ubuntu-latest
container: hadolint/hadolint:latest-debian
steps:
- uses: actions/checkout@v2
- name: Run Hadolint on the project
run: hadolint $(find . -iname "*Dockerfile*" -o -iwholename "./Dockerfile")
Loading

0 comments on commit a2fc245

Please sign in to comment.