Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for base image label/annotation #25

Merged
merged 1 commit into from
Jul 21, 2024
Merged
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
47 changes: 42 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ on:
types: [base-image-update]
```

See a working example of these workflows at [mthalman/docker-bump-action-example](https://github.com/mthalman/docker-bump-action-example).
See a working example of this workflow scenario in the [docker-bump-action-example](https://github.com/mthalman/docker-bump-action-example/blob/main/.github/workflows/monitor-dockerfile.yml) repo.

See [Examples](#examples) below for more usage patterns.
Specifying a Dockerfile is not the only way to configure this. See [Examples](#examples) below for more usage patterns.

### Action inputs

| Name | Description | Default |
| --- | --- | --- |
| `target-image-name` |**Required** Name of the image to check. | |
| `base-image-name` | Name of the base image the target image is based on. **Required** when `dockerfile` is not set. See [Image Name Derivation](#base-image-name-derivation). | |
| `dockerfile` | Path to the Dockerfile from which to derive image names. **Required** when `base-image-name` is not set. See [Image Name Derivation](#base-image-name-derivation). | |
| `dockerfile` | Path to the Dockerfile from which to derive image names. **Required** when `base-image-name` is not set and the target image does not contain a `org.opencontainers.image.base.name` label/annotation. See [Image Name Derivation](#base-image-name-derivation). | |
| `base-stage-name` | Name of the stage within the Dockerfile from which to derive the name of the base image. See [Image Name Derivation](#base-image-name-derivation). | |
| `arch` | Default architecture of the image | `amd64` |
| `arch` | Default architecture of the target image. This is used to resolve a multi-arch tag to a specifc image. | `amd64` |
| `repository` | The full name of the repository to send the dispatch. | `${{ github.repository }}` |
| `event-type` | A custom webhook event name. | `base-image-update` |
| `token` | An access token with the appropriate permissions. See [Token](#token). | `${{ github.token }}` |
Expand All @@ -71,7 +71,13 @@ You can also use a fine-grained personal access token (beta). It needs the follo

## Base Image Name Derivation

The base image name can either be provided explicitly via the `base-image-name` input or can be derived from the content of the Dockerfile specified by the `dockerfile` input. Be sure to examine the log output from the action to verify which image name it is using.
The base image name can either be provided explicitly via the `base-image-name` input or can be derived in one of two ways:
* the content of the Dockerfile specified by the `dockerfile` input.
* the target image contains `org.opencontainers.image.base.name` as a label or annotation.

Be sure to examine the log output from the action to verify which image name it is using.

### Derivation from Dockerfile

Depending on how you've structured your Dockerfiles (specifically for multi-stage Dockerfiles), the base image name that is derived by the algorithm may not be what you intended. If the base image name is not what you intended, you can override it via the `base-image-name` or `base-stage-name` inputs.

Expand Down Expand Up @@ -124,6 +130,33 @@ See #3 for support for multiple images.

## Examples

### Base Name Label/Annotation

If the `dockerfile` input is not provided, the action attempts to query the target image for the `org.opencontainers.image.base.name` label or annotation. If set, it will use that value as the base image name to check against.

This example Dockerfile shows how the label could be set.

> [!NOTE]
> Alternatively, labels and annotations can be set through various means when pushing an image to a registry:
> * [build-push-action labels](https://docs.docker.com/build/ci/github-actions/manage-tags-labels/)
> * [build-push-action annotations](https://docs.docker.com/build/ci/github-actions/annotations/)
> * [BuildKit annotations](https://github.com/moby/buildkit/blob/master/docs/annotations.md)

```Dockerfile
FROM alpine:latest
LABEL org.opencontainers.image.base.name=alpine:latest
```

The action is configured to only specify the target image name. That's all that's needed in order to determine the base image name.

```yaml
- uses: mthalman/docker-bump-action@v0
with:
target-image-name: ghcr.io/mthalman/docker-bump-action-example:latest
```

See a working example of this workflow scenario in the [docker-bump-action-example](https://github.com/mthalman/docker-bump-action-example/blob/main/.github/workflows/monitor-base-annotation.yml) repo.

### Explicitly set base stage name

In this example, the test stage is the last stage listed.
Expand Down Expand Up @@ -155,6 +188,8 @@ To configure the action to use `mcr.microsoft.com/dotnet/runtime:latest` as the

Indicating the base **stage** name rather than the base **image** name can be more convenient because it allows the image name in the Dockerfile to be updated without needing to also change the workflow file.

See a working example of this workflow scenario in the [docker-bump-action-example](https://github.com/mthalman/docker-bump-action-example/blob/main/.github/workflows/monitor-dockerfile-base-stage-name.yml) repo.

### Explicitly set base image name

In this example, the base image name used in the Dockerfile is dynamic so the action can't use the Dockerfile as input.
Expand All @@ -175,6 +210,8 @@ Here, the base image name is explicitly set to `alpine:latest`.
base-image-name: alpine:latest
```

See a working example of this workflow scenario in the [docker-bump-action-example](https://github.com/mthalman/docker-bump-action-example/blob/main/.github/workflows/monitor-base-image-name.yml) repo.

## Troubleshooting

### Error: `Resource not accessible by integration`
Expand Down
6 changes: 1 addition & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ runs:
$baseStage = "${{ inputs.base-stage-name }}"
$arch = "${{ inputs.arch }}"

if (-not $baseImage -and -not $dockerfile) {
throw "'dockerfile' input not provided. This is required when 'base-image-name' is not provided."
}

$dockerBumpCheckerVersion = "0.3.0"
$dockerBumpCheckerVersion = "0.4.0"

$containerName = "docker-bump-checker"
$containerSrcPath = "/src"
Expand Down
4 changes: 2 additions & 2 deletions container/check-image.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ Set-StrictMode -Version 2.0

function GetDigest($imageName) {
$digestCmd = "dredge manifest resolve $imageName --os linux --arch $Architecture"
$digest = $(InvokeTool $digestCmd "dredge manifest resolve failed")
$digest = $(InvokeTool $digestCmd)
return $digest
}

Import-Module $PSScriptRoot/common.psm1

$compareCmd = "dredge image compare layers --output json $BaseImage $TargetImage --os linux --arch $Architecture"
$layerComparisonStr = $(InvokeTool $compareCmd "dredge image compare failed")
$layerComparisonStr = $(InvokeTool $compareCmd)
$layerComparison = $layerComparisonStr | ConvertFrom-Json

$imageUpToDate = [bool]$($layerComparison.summary.targetIncludesAllBaseLayers)
Expand Down
4 changes: 2 additions & 2 deletions container/common.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function LogMessage ([string] $Message) {
Write-Output $Message | Out-File $env:HOME/log.txt -Append
}

function InvokeTool([string]$ToolCommand, [string] $ErrorMessage) {
function InvokeTool([string]$ToolCommand) {
LogMessage "Invoke: $ToolCommand"

# Reset $LASTEXITCODE in case it was tripped somewhere
Expand All @@ -17,7 +17,7 @@ function InvokeTool([string]$ToolCommand, [string] $ErrorMessage) {
$exitCode = $LASTEXITCODE
LogMessage "Result: $result"
if ($exitCode -ne 0) {
throw $ErrorMessage
throw "Command failed with exit code ${exitCode}: $ToolCommand"
}

return $result
Expand Down
2 changes: 1 addition & 1 deletion container/entrypoint.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ $ProgressPreference = 'SilentlyContinue'
Set-StrictMode -Version 2.0

if (-not $BaseImage) {
$BaseImage = $(& $PSScriptRoot/get-base-image.ps1 -DockerfilePath $DockerfilePath -BaseStageName $BaseStageName)
$BaseImage = $(& $PSScriptRoot/get-base-image.ps1 -DockerfilePath $DockerfilePath -BaseStageName $BaseStageName -TargetImage $targetImage -Architecture $Architecture)
}

$result = $(& $PSScriptRoot/check-image.ps1 -TargetImage $targetImage -BaseImage $BaseImage -Architecture $Architecture -DockerfilePath $DockerfilePath)
Expand Down
125 changes: 92 additions & 33 deletions container/get-base-image.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[cmdletbinding()]
param(
[Parameter(Mandatory = $True)]
[string]$DockerfilePath,
[string]$BaseStageName
[string]$BaseStageName,
[string]$TargetImage,
[string]$Architecture
)

$ErrorActionPreference = 'Stop'
Expand All @@ -11,49 +12,107 @@ Set-StrictMode -Version 2.0

Import-Module $PSScriptRoot/common.psm1

if (-not (Test-Path $DockerfilePath)) {
throw "Dockerfile path '$DockerfilePath' does not exist."
}
function GetBaseImageFromDockerfile() {
$dfspyArgs = @(
'query',
'from',
'-f',
$DockerfilePath
)

$dfspyArgs = @(
'query',
'from',
'-f',
$DockerfilePath
)
# If a stage name is provided we can directly filter the list-based output of the "query from" command.
# Otherwise, we need to derive the base image by finding the last stage of the Dockerfile and walking
# its parent chain. To do that, we need the graph layout.

if (-not $BaseStageName) {
LogMessage 'Stage name not provided. Will derive base image by walking parent chain.'
$dfspyArgs += '--layout'
$dfspyArgs += 'graph'
}

$dfspyArgsString = $dfspyArgs -join ' '

# If a stage name is provided we can directly filter the list-based output of the "query from" command.
# Otherwise, we need to derive the base image by finding the last stage of the Dockerfile and walking
# its parent chain. To do that, we need the graph layout.
$cmd = "dfspy $dfspyArgsString"
$fromOutput = $(InvokeTool $cmd)
$fromOutput = $fromOutput | ConvertFrom-Json

if (-not $BaseStageName) {
LogMessage 'Stage name not provided. Will derive base image by walking parent chain.'
$dfspyArgs += '--layout'
$dfspyArgs += 'graph'
if ($BaseStageName) {
$baseImage = $fromOutput | Where-Object { $_.PSObject.Properties.Name -contains "stageName" -and $_.stageName -eq $BaseStageName } | Select-Object -ExpandProperty imageName
if (-not $baseImage) {
throw "Could not find stage with name '$BaseStageName'."
}
} else {
# Find the last stage of the Dockerfile and walk its parent chain to find the base image
$currentNode = $fromOutput | Select-Object -Last 1

LogMessage "Deriving base image by walking parent chain from '$($currentNode.fromInstruction.imageName)'."

while ($currentNode.PSObject.Properties.Name -contains "parent") {
$currentNode = $currentNode.parent
}

$baseImage = $currentNode.fromInstruction.imageName
}

return $baseImage
}

$dfspyArgsString = $dfspyArgs -join ' '
function GetBaseImageFromAnnotation() {
$baseNameAnnotationKey = "org.opencontainers.image.base.name"
# Check whether a LABEL exists for the OCI annotation key
$inspectOutput = $(InvokeTool "dredge image inspect $TargetImage --os linux --arch $Architecture") | ConvertFrom-Json
Set-StrictMode -Off
if ($inspectOutput.config.Labels `
-and $inspectOutput.config.Labels.$baseNameAnnotationKey) {
$baseImage = $inspectOutput.config.Labels.$baseNameAnnotationKey
LogMessage "Found base image annotation from LABEL: '$baseImage'."
return $baseImage
}
Set-StrictMode -Version 2.0

# Check whether the annotation exists in the manifest
$manifest = $(InvokeTool "dredge manifest get $TargetImage") | ConvertFrom-Json

$cmd = "dfspy $dfspyArgsString"
$fromOutput = $(InvokeTool $cmd "dfspy failed")
$fromOutput = $fromOutput | ConvertFrom-Json
# The manifest may be for an image index, check that first.
Set-StrictMode -Off
if ($manifest.mediaType -eq "application/vnd.oci.image.index.v1+json") {
if ($manifest.annotations -and $manifest.annotations.$baseNameAnnotationKey) {
$baseImage = $manifest.Annotations.$baseNameAnnotationKey
LogMessage "Found base image annotation from manifest: '$baseImage'."
return $baseImage
}
}
Set-StrictMode -Version 2.0

if ($BaseStageName) {
$baseImage = $fromOutput | Where-Object { $_.PSObject.Properties.Name -contains "stageName" -and $_.stageName -eq $BaseStageName } | Select-Object -ExpandProperty imageName
if (-not $baseImage) {
throw "Could not find stage with name '$BaseStageName'."
$resolvedDigest = $(InvokeTool "dredge manifest resolve $TargetImage --os linux --arch $Architecture")
$manifest = $(InvokeTool "dredge manifest get $resolvedDigest") | ConvertFrom-Json
Set-StrictMode -Off
if ($manifest.annotations -and $manifest.annotations.$baseNameAnnotationKey) {
$baseImage = $manifest.Annotations.$baseNameAnnotationKey
LogMessage "Found base image annotation from manifest: '$baseImage'."
return $baseImage
}
} else {
# Find the last stage of the Dockerfile and walk its parent chain to find the base image
$currentNode = $fromOutput | Select-Object -Last 1
Set-StrictMode -Version 2.0

LogMessage "Deriving base image by walking parent chain from '$($currentNode.fromInstruction.imageName)'."
LogMessage "Could not find base image annotation '$baseNameAnnotationKey'."

while ($currentNode.PSObject.Properties.Name -contains "parent") {
$currentNode = $currentNode.parent
return $null
}

if ($DockerfilePath) {
if (-not (Test-Path $DockerfilePath)) {
throw "Dockerfile path '$DockerfilePath' does not exist."
}
$baseImage = GetBaseImageFromDockerfile
}
else {
# If a Dockerfile path is not provided, we need to rely on the base image annotation being set on
# the target image.
$baseImage = GetBaseImageFromAnnotation
}

$baseImage = $currentNode.fromInstruction.imageName
if (-not $baseImage) {
throw "Could not derive base image name."
}

LogMessage "Using base image name '$baseImage'."
Expand Down
2 changes: 1 addition & 1 deletion container/tests/check-image.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,6 @@ Describe 'Get result' {
It 'Given a failed dredge command, it throws an error' {
Mock Invoke-Expression { $global:LASTEXITCODE = 1 } -ParameterFilter { $Command -like "dredge *" } -ModuleName common

{ & $targetScript -TargetImage $targetImage -BaseImage $baseImage -Architecture $architecture } | Should -Throw "dredge image compare failed"
{ & $targetScript -TargetImage $targetImage -BaseImage $baseImage -Architecture $architecture } | Should -Throw "Command failed with exit code 1: dredge image compare layers --output json bar foo --os linux --arch amd64"
}
}
Loading