From a87226efb2c7b371b8c30e63bdf073cff79958b1 Mon Sep 17 00:00:00 2001 From: Jernej Porenta Date: Thu, 22 Sep 2022 15:29:57 +0200 Subject: [PATCH] Add support for Gitlab CI (#39) - adding support for Gitlab CI --- README.md | 1 + parser/gitlabci.go | 80 +++++++++++++++++++++++++++ parser/gitlabci_test.go | 119 ++++++++++++++++++++++++++++++++++++++++ parser/parser.go | 1 + testdata/gitlabci.yml | 24 ++++++++ 5 files changed, 225 insertions(+) create mode 100644 parser/gitlabci.go create mode 100644 parser/gitlabci_test.go create mode 100644 testdata/gitlabci.yml diff --git a/README.md b/README.md index 150f0e9cce..73d8a23a32 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Cargo, Go modules, NPM, Pip, or Yarn, but for CI/CD workflows. Ratchet supports: - Circle CI - GitHub Actions +- GitLab CI - Google Cloud Build diff --git a/parser/gitlabci.go b/parser/gitlabci.go new file mode 100644 index 0000000000..b3f94149b1 --- /dev/null +++ b/parser/gitlabci.go @@ -0,0 +1,80 @@ +package parser + +import ( + "fmt" + + "github.com/sethvargo/ratchet/resolver" + "gopkg.in/yaml.v3" +) + +type GitLabCI struct{} + +// Parse pulls the image references from GitLab CI configuration files. +// It does not support references with variables. + +func (C *GitLabCI) Parse(m *yaml.Node) (*RefsList, error) { + var refs RefsList + var imageRef *yaml.Node + + // GitLab CI global top level keywords + var globalKeywords = map[string]struct{}{ + "default": {}, + "include": {}, + "stages": {}, + "variables": {}, + "workflow": {}, + } + + if m == nil { + return nil, nil + } + + if m.Kind != yaml.DocumentNode { + return nil, fmt.Errorf("expected document node, got %v", m.Kind) + } + + // Top-level object map + for _, docMap := range m.Content { + if docMap.Kind != yaml.MappingNode { + continue + } + // jobs names + for i, keysMap := range docMap.Content { + + // exclude global keywords + if _, hit := globalKeywords[keysMap.Value] ; hit || (keysMap.Value == "") { + continue + } + + job := docMap.Content[i+1] + if job.Kind != yaml.MappingNode { + continue + } + + for k, property := range job.Content { + if property.Value == "image" { + + image := job.Content[k+1] + + // match image reference with name key + if image.Kind == yaml.MappingNode { + + for j, nameRef := range image.Content { + if nameRef.Value == "name" { + imageRef = image.Content[j+1] + break + } + } + } else { + imageRef = image + } + + ref := resolver.NormalizeContainerRef(imageRef.Value) + refs.Add(ref, imageRef) + } + } + } + } + + return &refs, nil +} diff --git a/parser/gitlabci_test.go b/parser/gitlabci_test.go new file mode 100644 index 0000000000..24539599d1 --- /dev/null +++ b/parser/gitlabci_test.go @@ -0,0 +1,119 @@ +package parser + +import ( + "fmt" + "reflect" + "testing" +) + +func TestGitLabCI_Parse(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + in string + exp []string + }{ + { + name: "no_image_reference", + in: ` +stages: + - plan + - destroy + +workflow: + rules: + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_BRANCH + +variables: + VAR1: example +`, + exp: []string{}, + }, + { + name: "wrong_image_reference", + in: ` +test_job: + stage: lint + variables: + SCAN_DIR: . + image: $CI_REGISTRY/image:tag +`, + exp: []string{ + "container://$CI_REGISTRY/image:tag", + }, + }, + { + name: "multiline_image_ref", + in: ` +test_job: + stage: test + variables: + SCAN_DIR: . + image: + name: alpine:3.15.0 + entrypoint: [""] + script: + - printenv +`, + exp: []string{ + "container://alpine:3.15.0", + }, + }, + { + name: "job_with_include", + in: ` +.test:base: + stage: test + image: python + retry: + max: 1 + variables: + VAR1: true + script: + - test command + +job: + extends: + - .test:base + image: node:12 + stage: test + script: + - test command + +job2: + image: gcr.io/project/image:tag + stage: test + script: + - test command +`, + exp: []string{ + "container://gcr.io/project/image:tag", + "container://node:12", + "container://python", + }, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + m := helperStringToYAML(t, tc.in) + + refs, err := new(GitLabCI).Parse(m) + + if err != nil { + fmt.Println(refs) + t.Fatal(err) + } + + if got, want := refs.Refs(), tc.exp; !reflect.DeepEqual(got, want) { + t.Errorf("expected %q to be %q", got, want) + } + }) + } +} diff --git a/parser/parser.go b/parser/parser.go index d9464b141c..fba594cf90 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -28,6 +28,7 @@ var parserFactory = map[string]func() Parser{ "actions": func() Parser { return new(Actions) }, "circleci": func() Parser { return new(CircleCI) }, "cloudbuild": func() Parser { return new(CloudBuild) }, + "gitlabci": func() Parser { return new(GitLabCI) }, } // For returns the parser that corresponds to the given name. diff --git a/testdata/gitlabci.yml b/testdata/gitlabci.yml new file mode 100644 index 0000000000..465cf85e54 --- /dev/null +++ b/testdata/gitlabci.yml @@ -0,0 +1,24 @@ +--- +stages: + - build + - test + +build-code-job: + stage: build + image: + name: gcr.io/distroless/static-debian11:nonroot + entrypoint: [""] + script: + - echo "Job 1" + +test-code-job1: + stage: test + image: node:12 + script: + - echo "Job 2" + +test-code-job2: + stage: test + image: amazon/aws-cli:latest + script: + - echo "Job 3"