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 pkg/ast/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ type DockerImageInfo struct {
Namespace string
Name string
Tag string
Digest string // SHA256 digest if present (e.g., "sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba")

FullPath string
}
Expand Down
12 changes: 5 additions & 7 deletions pkg/parser/dockerImageParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/CircleCI-Public/circleci-yaml-language-server/pkg/ast"
)

var dockerImageRegex = regexp.MustCompile(`^([a-z0-9\-_]+\/)?([a-z0-9\-_]+)(:(.*))?$`)
var dockerImageRegex = regexp.MustCompile(`^([a-z0-9\-_]+\/)?([a-z0-9\-_]+)(:([^@]*))?(@(.+))?$`)
var aliasRemover = regexp.MustCompile(`^&[a-zA-Z0-9\-_]+\s*`)

func ParseDockerImageValue(value string) ast.DockerImageInfo {
Expand All @@ -18,13 +18,15 @@ func ParseDockerImageValue(value string) ast.DockerImageInfo {
Namespace: "library",
Name: "",
Tag: "",
Digest: "",
FullPath: value,
}
}

namespace := imageName[0][1]
repository := imageName[0][2]
tag := imageName[0][3]
tag := imageName[0][4]
digest := imageName[0][6]

if namespace == "" {
namespace = "library"
Expand All @@ -33,15 +35,11 @@ func ParseDockerImageValue(value string) ast.DockerImageInfo {
namespace = namespace[:len(namespace)-1]
}

if tag != "" {
// The regex includes the leading ":", just snip it
tag = tag[1:]
}

return ast.DockerImageInfo{
Namespace: namespace,
Name: repository,
Tag: tag,
Digest: digest,
FullPath: value,
}
}
120 changes: 120 additions & 0 deletions pkg/parser/dockerImageParser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "library",
Tag: "",
Name: "node",
Digest: "",
FullPath: "node",
},
},
Expand All @@ -39,6 +40,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "library",
Tag: "",
Name: "node",
Digest: "",
FullPath: "node:",
},
},
Expand All @@ -52,6 +54,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "library",
Tag: "12",
Name: "node",
Digest: "",
FullPath: "node:12",
},
},
Expand All @@ -65,6 +68,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "library",
Tag: "latest",
Name: "node",
Digest: "",
FullPath: "node:latest",
},
},
Expand All @@ -78,6 +82,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "cimg",
Tag: "latest",
Name: "go",
Digest: "",
FullPath: "cimg/go:latest",
},
},
Expand All @@ -91,6 +96,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "cimg",
Tag: "",
Name: "go",
Digest: "",
FullPath: "cimg/go",
},
},
Expand All @@ -104,6 +110,7 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "cimg",
Tag: "",
Name: "go",
Digest: "",
FullPath: "cimg/go:",
},
},
Expand All @@ -117,9 +124,122 @@ func Test_parseDockerImageValue(t *testing.T) {
Namespace: "cimg",
Tag: "<<parameters.go_version>>",
Name: "go",
Digest: "",
FullPath: "cimg/go:<<parameters.go_version>>",
},
},

{
name: "SHA256 digest without tag",
args: args{
value: "cimg/node@sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba",
},
want: ast.DockerImageInfo{
Namespace: "cimg",
Tag: "",
Name: "node",
Digest: "sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba",
FullPath: "cimg/node@sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba",
},
},

{
name: "SHA256 digest with tag",
args: args{
value: "cimg/node:22.11.0@sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba",
},
want: ast.DockerImageInfo{
Namespace: "cimg",
Tag: "22.11.0",
Name: "node",
Digest: "sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba",
FullPath: "cimg/node:22.11.0@sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba",
},
},

{
name: "Library image with SHA256 digest",
args: args{
value: "node:18@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
},
want: ast.DockerImageInfo{
Namespace: "library",
Tag: "18",
Name: "node",
Digest: "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
FullPath: "node:18@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
},
},

{
name: "Library image with only SHA256 digest",
args: args{
value: "node@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
},
want: ast.DockerImageInfo{
Namespace: "library",
Tag: "",
Name: "node",
Digest: "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
FullPath: "node@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
},
},

{
name: "Parse any string after @ as digest - short string",
args: args{
value: "cimg/go:1.24@foo",
},
want: ast.DockerImageInfo{
Namespace: "cimg",
Name: "go",
Tag: "1.24",
Digest: "foo",
FullPath: "cimg/go:1.24@foo",
},
},

{
name: "Parse any string after @ as digest - no sha256 prefix",
args: args{
value: "cimg/node:18@abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
},
want: ast.DockerImageInfo{
Namespace: "cimg",
Name: "node",
Tag: "18",
Digest: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
FullPath: "cimg/node:18@abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
},
},

{
name: "Parse any string after @ as digest - wrong hash length",
args: args{
value: "cimg/go:latest@sha256:abc123",
},
want: ast.DockerImageInfo{
Namespace: "cimg",
Name: "go",
Tag: "latest",
Digest: "sha256:abc123",
FullPath: "cimg/go:latest@sha256:abc123",
},
},

{
name: "Parse any string after @ as digest - non-hex characters",
args: args{
value: "node:alpine@sha256:ghijklmnopqrstuvwxyz1234567890abcdef1234567890abcdef1234567890",
},
want: ast.DockerImageInfo{
Namespace: "library",
Name: "node",
Tag: "alpine",
Digest: "sha256:ghijklmnopqrstuvwxyz1234567890abcdef1234567890abcdef1234567890",
FullPath: "node:alpine@sha256:ghijklmnopqrstuvwxyz1234567890abcdef1234567890abcdef1234567890",
},
},
}

for _, tt := range tests {
Expand Down
7 changes: 7 additions & 0 deletions pkg/parser/validate/dockerImage.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package validate

import (
"regexp"
"strings"

"github.com/CircleCI-Public/circleci-yaml-language-server/pkg/ast"
Expand All @@ -11,6 +12,12 @@ import (
"go.lsp.dev/protocol"
)

var validDigestRegex = regexp.MustCompile(`^sha256:[a-f0-9]{64}$`)

func isValidDockerDigest(digest string) bool {
return validDigestRegex.MatchString(digest)
}

func DoesDockerImageExists(img *ast.DockerImage, cache *utils.DockerCache, api dockerhub.DockerHubAPI) bool {
cachedDockerImage := cache.Get(img.Image.FullPath)

Expand Down
104 changes: 104 additions & 0 deletions pkg/parser/validate/dockerImage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,110 @@ workflows:
image-tag: tag
`,
},
{
Name: "Should give no diagnostic on valid SHA256 digest with tag",

Diagnostics: []ComparableDiagnostic{},

MockAPI: DockerHubMock{},

YamlContent: `version: 2.1

executors:
some-executor:
docker:
- image: cimg/node:22.11.0@sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba`,
},
{
Name: "Should give no diagnostic on valid SHA256 digest without tag",

Diagnostics: []ComparableDiagnostic{},

MockAPI: DockerHubMock{},

YamlContent: `version: 2.1

executors:
some-executor:
docker:
- image: cimg/node@sha256:76aae59c6259672ab68819b8960de5ef571394681089eab2b576f85f080c73ba`,
},
{
Name: "Should give error on invalid digest format - too short",

Diagnostics: []ComparableDiagnostic{
{
Severity: protocol.DiagnosticSeverityError,
Message: "Invalid Docker image digest format \"foo\". Expected format: sha256:<64 hex characters>",
},
},

MockAPI: DockerHubMock{},

YamlContent: `version: 2.1

executors:
some-executor:
docker:
- image: cimg/go:1.24@foo`,
},
{
Name: "Should give error on invalid digest format - wrong prefix",

Diagnostics: []ComparableDiagnostic{
{
Severity: protocol.DiagnosticSeverityError,
Message: "Invalid Docker image digest format \"abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890\". Expected format: sha256:<64 hex characters>",
},
},

MockAPI: DockerHubMock{},

YamlContent: `version: 2.1

executors:
some-executor:
docker:
- image: cimg/node:18@abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890`,
},
{
Name: "Should give error on invalid digest format - wrong hash length",

Diagnostics: []ComparableDiagnostic{
{
Severity: protocol.DiagnosticSeverityError,
Message: "Invalid Docker image digest format \"sha256:abc123\". Expected format: sha256:<64 hex characters>",
},
},

MockAPI: DockerHubMock{},

YamlContent: `version: 2.1

executors:
some-executor:
docker:
- image: cimg/go:latest@sha256:abc123`,
},
{
Name: "Should give error on invalid digest format - non-hex characters",

Diagnostics: []ComparableDiagnostic{
{
Severity: protocol.DiagnosticSeverityError,
Message: "Invalid Docker image digest format \"sha256:ghijklmnopqrstuvwxyz1234567890abcdef1234567890abcdef1234567890\". Expected format: sha256:<64 hex characters>",
},
},

MockAPI: DockerHubMock{},

YamlContent: `version: 2.1

executors:
some-executor:
docker:
- image: node:alpine@sha256:ghijklmnopqrstuvwxyz1234567890abcdef1234567890abcdef1234567890`,
},
}

for _, tt := range testCases {
Expand Down
Loading