Skip to content

Commit

Permalink
Add support for CircleCI (#13)
Browse files Browse the repository at this point in the history
* Fix update command help text

* Add support for CircleCI
  • Loading branch information
sethvargo authored May 20, 2022
1 parent 74dd3ae commit 31c89aa
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 0 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ Ratchet is a tool for improving the security of CI/CD workflows by automating
the process of pinning and unpinning upstream versions. It's like Bundler,
Cargo, Go modules, NPM, Pip, or Yarn, but for CI/CD workflows. Ratchet supports:

- Circle CI
- GitHub Actions
- Google Cloud Build


## Problem statement

Most CI/CD systems are one layer of indirection away from `curl | sudo bash`.
Expand Down Expand Up @@ -52,6 +54,9 @@ image: 'ubuntu@sha256:47f14534bda344d9fe6ffd6effb95eefe579f4be0d508b7445cf77f61a
# pin the input file
./ratchet pin workflow.yml

# pin a circleci file
./ratchet pin -parser circleci circleci.yml

# pin a cloudbuild file
./ratchet pin -parser cloudbuild cloudbuild.yml

Expand All @@ -75,6 +80,9 @@ image: 'ubuntu@sha256:47f14534bda344d9fe6ffd6effb95eefe579f4be0d508b7445cf77f61a
# update the input file
./ratchet update workflow.yml

# update a circleci file
./ratchet update -parser circleci circleci.yml

# update a cloudbuild file
./ratchet update -parser cloudbuild cloudbuild.yml

Expand Down
7 changes: 7 additions & 0 deletions command/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"flag"
"fmt"
"os"
"strings"

"github.com/sethvargo/ratchet/parser"
"github.com/sethvargo/ratchet/resolver"
Expand Down Expand Up @@ -37,6 +39,11 @@ func (c *UpdateCommand) Desc() string {

func (c *UpdateCommand) Flags() *flag.FlagSet {
f := c.PinCommand.Flags()
f.Usage = func() {
fmt.Fprintf(os.Stderr, "%s\n\n", strings.TrimSpace(updateCommandHelp))
f.PrintDefaults()
}

return f
}

Expand Down
75 changes: 75 additions & 0 deletions parser/circleci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package parser

import (
"fmt"

"github.com/sethvargo/ratchet/resolver"
"gopkg.in/yaml.v3"
)

type CircleCI struct{}

// Parse pulls the CircleCI refs from the document. Unfortunately it does not
// process "orbs" because there is no documented API for resolving orbs to an
// absolute version.
func (C *CircleCI) Parse(m *yaml.Node) (*RefsList, error) {
var refs RefsList

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: and executors: keyword
for i, jobsMap := range docMap.Content {
if jobsMap.Value != "jobs" && jobsMap.Value != "executors" {
continue
}

// Individual job names
jobs := docMap.Content[i+1]
if jobs.Kind != yaml.MappingNode {
continue
}

for _, jobMap := range jobs.Content {
if jobMap.Kind != yaml.MappingNode {
continue
}

for j, sub := range jobMap.Content {
// CI service container, should be resolved as a Docker reference.
// This is a map, so the container value is nested a bit deeper.
if sub.Value == "docker" {
servicesMap := jobMap.Content[j+1]
for _, subMap := range servicesMap.Content {
if subMap.Kind != yaml.MappingNode {
continue
}

for k, property := range subMap.Content {
if property.Value == "image" {
image := subMap.Content[k+1]
ref := resolver.NormalizeContainerRef(image.Value)
refs.Add(ref, image)
break
}
}
}
}
}
}
}
}

return &refs, nil
}
69 changes: 69 additions & 0 deletions parser/circleci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package parser

import (
"reflect"
"testing"
)

func TestCircleCI_Parse(t *testing.T) {
t.Parallel()

cases := []struct {
name string
in string
exp []string
}{
{
name: "mostly_empty_file",
in: `
executors:
`,
exp: []string{},
},
{
name: "executor",
in: `
executors:
my-executor:
docker:
- image: 'docker://ubuntu:20.04'
`,
exp: []string{
"container://ubuntu:20.04",
},
},
{
name: "job",
in: `
jobs:
my-job:
docker:
- image: 'ubuntu:20.04'
- image: 'ubuntu:22.04'
`,
exp: []string{
"container://ubuntu:20.04",
"container://ubuntu:22.04",
},
},
}

for _, tc := range cases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

m := helperStringToYAML(t, tc.in)

refs, err := new(CircleCI).Parse(m)
if err != nil {
t.Fatal(err)
}

if got, want := refs.Refs(), tc.exp; !reflect.DeepEqual(got, want) {
t.Errorf("expected %q to be %q", got, want)
}
})
}
}
1 change: 1 addition & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Parser interface {

var parserFactory = map[string]func() Parser{
"actions": func() Parser { return new(Actions) },
"circleci": func() Parser { return new(CircleCI) },
"cloudbuild": func() Parser { return new(CloudBuild) },
}

Expand Down
16 changes: 16 additions & 0 deletions testdata/circleci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: '2.1'

orbs:
my-orb: 'circleci/hello-build@0.0.5'
my-other-orb: 'circleci/hello-build@0.0.3'

executors:
my-executor:
docker:
- image: 'cimg/base:2022.05-22.04'

jobs:
build:
docker:
- image: 'cimg/base:2022.05-22.04'
- image: 'ubuntu:20.04'

0 comments on commit 31c89aa

Please sign in to comment.