pipedawg 0.5.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53733705295ce1c9b2ae80fc1c8368e7e08e7d7a0e1fcf3727fe972dd5714fb1
4
- data.tar.gz: 10e86751592488fbb9972605892d79af662e60e3a3da070292cf4933f31b7981
3
+ metadata.gz: 881d25335110dc811ee1570b6c3e909cee4bbf7e8209010d0287a159ee27d254
4
+ data.tar.gz: 0a1b664c0c074832b38a9ffb4700af77145ca1f102ca505d58eff2f558c24ca7
5
5
  SHA512:
6
- metadata.gz: 11584d541a5d63f54761dee9ca878e3c79ba168ee1e5e93ec9ddccd559d869672a900a6aa25d966df4ef807a56ac0c75385644baf3bce5498ac7a68a29fd8a9a
7
- data.tar.gz: 97f20d550f1cbe4bf298a2f6884f02754cd385e317d777887d11a8748d7436d8bb6db035944c6f59d2593b4e3a615db76f28c678bb392cf16ef3e6c6f92770a9
6
+ metadata.gz: b5c2156cfe2a3e73c31a974fc7bcc51144f28d1ba0a9ffef134d585dfd2663071682f84254f908e4df86b09e8ca8ae06a6bd422ebe4619ee554722a5eff72af4
7
+ data.tar.gz: 73058b94b1c44615aa5c5ede80dd010013021a7eab68b653019975464ed9b84b445f625897b4e08ee6982a0a537fee687171941316d5e8749117a767dc954116
data/README.md CHANGED
@@ -32,7 +32,6 @@ Example:
32
32
  #!/usr/bin/env ruby
33
33
  # frozen_string_literal: true
34
34
 
35
- # print_pipeline.rb
36
35
  require 'pipedawg'
37
36
 
38
37
  gem_job = Pipedawg::Job.new(
@@ -42,31 +41,32 @@ gem_job = Pipedawg::Job.new(
42
41
  script: ['bundle install', 'gem build *.gemspec']
43
42
  )
44
43
 
45
- kaniko_job = Pipedawg::KanikoJob.new(
44
+ kaniko_build_job = Pipedawg::Job::Kaniko::Build.new(
46
45
  'build:kaniko',
47
- {needs: ['build:gem'], retry: 2},
48
- {context:'${CI_PROJECT_DIR}/docker',external_files: {'*.gem':'gems'}}
46
+ needs: ['build:gem'],
47
+ retry: 2,
48
+ context:'${CI_PROJECT_DIR}/docker',
49
+ external_files: {'*.gem':'gems'},
50
+ debug: false
49
51
  )
50
52
 
51
- pipeline = Pipedawg::Pipeline.new 'build:image', jobs: [gem_job, kaniko_job]
53
+ pipeline = Pipedawg::Pipeline.new 'build:image', jobs: [gem_job, kaniko_build_job]
52
54
  puts pipeline.to_yaml
55
+ pipeline.to_yaml_file('/tmp/pipeline.yaml')
53
56
  ```
54
57
 
55
58
  ```console
56
- $ chmod +x print_pipeline.rb
57
- $ ./print_pipeline.rb
59
+ $ cat /tmp/pipeline.yaml
58
60
  ---
59
61
  stages:
60
62
  - '1'
61
63
  - '2'
62
- workflow: {}
63
64
  build:gem:
64
65
  artifacts:
65
66
  - "*.gem"
66
67
  cache: {}
67
68
  image: ruby
68
69
  needs: []
69
- rules: []
70
70
  script:
71
71
  - bundle install
72
72
  - gem build *.gemspec
@@ -75,16 +75,19 @@ build:gem:
75
75
  build:kaniko:
76
76
  artifacts: {}
77
77
  cache: {}
78
+ image:
79
+ entrypoint:
80
+ - ''
81
+ name: gcr.io/kaniko-project/executor:debug
78
82
  needs:
79
83
  - build:gem
80
84
  retry: 2
81
- rules: []
82
85
  script:
83
86
  - echo "{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}"
84
87
  > "/kaniko/.docker/config.json"
85
88
  - cp "*.gem" "${CI_PROJECT_DIR}/docker/gems"
86
89
  - '"/kaniko/executor" --context "${CI_PROJECT_DIR}/docker" --dockerfile "Dockerfile"
87
- --no-push'
90
+ --destination ${CI_REGISTRY_IMAGE}:latest'
88
91
  stage: '2'
89
92
  tags: []
90
93
  ```
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ class Helm
6
+ # Pipedawg::Job::Helm::Copy class
7
+ class Copy < Job::Helm
8
+ def initialize(name, opts = {})
9
+ opts = {
10
+ chart: name,
11
+ destinations: [{ user: nil, password: nil, url: nil }],
12
+ password: nil, url: nil, user: nil, version: nil
13
+ }.merge(opts)
14
+ super name, opts
15
+ update
16
+ end
17
+
18
+ def update
19
+ opts[:script] = debug + pull + (opts[:destinations].map { |d| push(d) }).flatten(1)
20
+ end
21
+
22
+ private
23
+
24
+ def pull
25
+ case opts[:url]
26
+ when nil
27
+ []
28
+ when %r{^oci://}
29
+ pull_oci
30
+ else
31
+ pull_classic
32
+ end
33
+ end
34
+
35
+ def push(destination)
36
+ case destination[:url]
37
+ when nil
38
+ []
39
+ when %r{^oci://}
40
+ push_oci(destination)
41
+ else
42
+ push_classic(destination)
43
+ end
44
+ end
45
+
46
+ def pull_oci # rubocop:disable Metrics/AbcSize
47
+ script = []
48
+ if opts[:url] && opts[:chart] && opts[:version]
49
+ script = ['export HELM_EXPERIMENTAL_OCI=1']
50
+ script << login_oci(opts) if opts[:user] && opts[:password]
51
+ script << "\"#{opts[:command]}\" pull \"#{opts[:url]}/#{opts[:chart]}\" --version \"#{opts[:version]}\""
52
+ end
53
+ script
54
+ end
55
+
56
+ def push_oci(destination) # rubocop:disable Metrics/AbcSize
57
+ script = []
58
+ if destination[:url] && opts[:chart] && opts[:version]
59
+ script = ['export HELM_EXPERIMENTAL_OCI=1']
60
+ script << login_oci(destination) if destination[:user] && destination[:password]
61
+ script << "\"#{opts[:command]}\" push \"#{opts[:chart]}-#{opts[:version]}.tgz\" \"#{destination[:url]}\""
62
+ end
63
+ script
64
+ end
65
+
66
+ def login_oci(login_opts)
67
+ require 'uri'
68
+ "echo \"#{login_opts[:password]}\" | \"#{opts[:command]}\" registry login --username \"#{login_opts[:user]}\" --password-stdin \"#{URI(login_opts[:url]).host}\"" # rubocop:disable Layout/LineLength
69
+ end
70
+
71
+ def pull_classic # rubocop:disable Metrics/AbcSize
72
+ script = []
73
+ if opts[:url] && opts[:chart] && opts[:version]
74
+ suffix = login_classic(opts)
75
+ script << "\"#{opts[:command]}\" repo add source \"#{opts[:url]}\"#{suffix}"
76
+ script << "\"#{opts[:command]}\" repo update"
77
+ script << "\"#{opts[:command]}\" pull \"source/#{opts[:chart]}\" --version \"#{opts[:version]}\""
78
+ end
79
+ script
80
+ end
81
+
82
+ def push_classic(destination)
83
+ script = []
84
+ if destination[:url] && opts[:chart] && opts[:version]
85
+ script << plugin_classic
86
+ suffix = login_classic(destination)
87
+ script << "\"#{opts[:command]}\" cm-push \"#{opts[:chart]}-#{opts[:version]}.tgz\" \"#{destination[:url]}\"#{suffix}" # rubocop:disable Layout/LineLength
88
+ end
89
+ script
90
+ end
91
+
92
+ def login_classic(login_opts)
93
+ if login_opts[:user] && login_opts[:password]
94
+ " --username \"#{login_opts[:user]}\" --password \"#{login_opts[:password]}\""
95
+ else
96
+ ''
97
+ end
98
+ end
99
+
100
+ def plugin_classic
101
+ "\"#{opts[:command]}\" plugin list | grep -q cm-push || \"#{opts[:command]}\" plugin install https://github.com/chartmuseum/helm-push"
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ # Pipedawg::Job::Helm class
6
+ class Helm < Job
7
+ def initialize(name, opts = {})
8
+ opts = {
9
+ command: 'helm',
10
+ image: { entrypoint: [''], name: 'alpine/helm' }
11
+ }.merge(opts)
12
+ super name, opts
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ class Kaniko
6
+ # Pipedawg::Job::Kaniko::Build class
7
+ class Build < Job::Kaniko
8
+ def initialize(name, opts = {}) # rubocop:disable Metrics/MethodLength
9
+ opts = {
10
+ build_args: {},
11
+ config: { auths: { '$CI_REGISTRY': { username: '$CI_REGISTRY_USER', password: '$CI_REGISTRY_PASSWORD' } } },
12
+ config_file: '/kaniko/.docker/config.json', context: '${CI_PROJECT_DIR}',
13
+ destinations: ['${CI_REGISTRY_IMAGE}:latest'], dockerfile: 'Dockerfile', external_files: {}, flags: [],
14
+ ignore_paths: [], insecure_registries: [], options: {}, registry_certificates: {}, registry_mirrors: [],
15
+ skip_tls_verify_registry: [], trusted_ca_cert_source_files: [],
16
+ trusted_ca_cert_target_file: '/kaniko/ssl/certs/ca-certificates.crt'
17
+ }.merge(opts)
18
+ super name, opts
19
+ update
20
+ end
21
+
22
+ def update
23
+ require 'json'
24
+ opts[:script] = debug + config + cert_copies + file_copies + Array(kaniko_cmd)
25
+ end
26
+
27
+ private
28
+
29
+ def config
30
+ ["echo #{opts[:config].to_json.inspect} > \"#{opts[:config_file]}\""]
31
+ end
32
+
33
+ def cert_copies
34
+ Array(opts[:trusted_ca_cert_source_files]).map do |cert|
35
+ "cat \"#{cert}\" >> \"#{opts[:trusted_ca_cert_target_file]}\""
36
+ end
37
+ end
38
+
39
+ def file_copies
40
+ opts[:external_files].map do |source, dest|
41
+ "cp \"#{source}\" \"#{opts[:context]}/#{dest}\""
42
+ end
43
+ end
44
+
45
+ def kaniko_cmd # rubocop:disable Metrics/AbcSize
46
+ ["\"#{opts[:command]}\" --context \"#{opts[:context]}\"",
47
+ "--dockerfile \"#{opts[:dockerfile]}\"", flags, options, build_args,
48
+ ignore_paths, insecure_registries, registry_certificates, registry_mirrors,
49
+ destinations, skip_tls_verify_registries].reject(&:empty?).join(' ')
50
+ end
51
+
52
+ def flags
53
+ flags = opts[:flags].clone
54
+ flags << 'no-push' if opts[:destinations].empty?
55
+ flags.uniq.map { |f| "--#{f}" }.join(' ')
56
+ end
57
+
58
+ def options
59
+ opts[:options].map { |k, v| "--#{k}=\"#{v}\"" }.join(' ')
60
+ end
61
+
62
+ def build_args
63
+ opts[:build_args].map { |k, v| "--build-arg #{k}=\"#{v}\"" }.join(' ')
64
+ end
65
+
66
+ def ignore_paths
67
+ Array(opts[:ignore_paths]).map { |p| "--ignore-path #{p}" }.join(' ')
68
+ end
69
+
70
+ def insecure_registries
71
+ Array(opts[:insecure_registries]).map do |r|
72
+ "--insecure-registry #{r}"
73
+ end.join(' ')
74
+ end
75
+
76
+ def registry_certificates
77
+ opts[:registry_certificates].map do |k, v|
78
+ "--registry-certificate #{k}=\"#{v}\""
79
+ end.join(' ')
80
+ end
81
+
82
+ def registry_mirrors
83
+ Array(opts[:registry_mirrors]).map { |r| "--registry-mirror #{r}" }.join(' ')
84
+ end
85
+
86
+ def destinations
87
+ opts[:destinations].map { |d| "--destination #{d}" }.join(' ')
88
+ end
89
+
90
+ def skip_tls_verify_registries
91
+ Array(opts[:skip_tls_verify_registry]).map do |r|
92
+ "--skip-tls-verify-registry #{r}"
93
+ end.join(' ')
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ # Pipedawg::Job::Kaniko class
6
+ class Kaniko < Job
7
+ def initialize(name, opts = {})
8
+ opts = {
9
+ command: '/kaniko/executor',
10
+ image: { entrypoint: [''], name: 'gcr.io/kaniko-project/executor:debug' }
11
+ }.merge(opts)
12
+ super name, opts
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ class Qualys
6
+ # Pipedawg::Job::Qualys::Scan class
7
+ class Scan < Job::Qualys # rubocop:disable Metrics/ClassLength
8
+ def initialize(name, opts = {})
9
+ opts = {
10
+ acceptable_risk: '${QUALYS_ACCEPTABLE_IMAGE_RISK}',
11
+ artifacts: { expire_in: '1 month', paths: ['software.json', 'vulnerabilities.json'], when: 'always' },
12
+ config: { auths: { '$CI_REGISTRY': { username: '$CI_REGISTRY_USER', password: '$CI_REGISTRY_PASSWORD' } } },
13
+ gateway: '${QUALYS_GATEWAY}', image: nil, password: '${QUALYS_PASSWORD}',
14
+ scan_image: '${QUALYS_IMAGE}', scan_target_prefix: 'qualys_scan_target',
15
+ user: '${QUALYS_USERNAME}', variables: { GIT_STRATEGY: 'clone' }
16
+ }.merge(opts)
17
+ super name, opts
18
+ update
19
+ end
20
+
21
+ def update # rubocop:disable Metrics/AbcSize
22
+ require 'json'
23
+ opts[:script] =
24
+ debug + config + image + clean_config + token + scan_start +
25
+ scan_complete + artifacts + severities + outputs
26
+ end
27
+
28
+ private
29
+
30
+ def debug # rubocop:disable Metrics/MethodLength
31
+ if opts[:debug]
32
+ super + [
33
+ 'echo Qualys settings:', "echo Qualys gateway: \"#{opts[:gateway]}\"",
34
+ "echo Qualys username: \"#{opts[:user]}\"",
35
+ "if [ \"#{opts[:password]}\" != '' ]; then " \
36
+ 'echo Qualys password is not empty; else ' \
37
+ 'echo Qualys password is not set; exit 1; fi'
38
+ ]
39
+ else
40
+ []
41
+ end
42
+ end
43
+
44
+ def config
45
+ ['export CONFIG=$(mktemp -d)', "echo #{opts[:config].to_json.inspect} > \"${CONFIG}/config.json\""]
46
+ end
47
+
48
+ def image
49
+ [
50
+ "image_target=\"#{opts[:scan_target_prefix]}:$(echo #{opts[:scan_image]} | sed 's/^[^/]*\\///'| sed 's/[:/]/-/g')\"", # rubocop:disable Layout/LineLength
51
+ "docker --config=\"${CONFIG}\" pull \"#{opts[:scan_image]}\"",
52
+ "docker image tag \"#{opts[:scan_image]}\" \"${image_target}\"",
53
+ "image_id=$(docker inspect --format=\"{{index .Id}}\" \"#{opts[:scan_image]}\" | cut -c8-19)",
54
+ 'echo "Image ID: ${image_id}"'
55
+ ]
56
+ end
57
+
58
+ def clean_config
59
+ [
60
+ 'rm -f "${CONFIG}/config.json"',
61
+ 'rmdir "${CONFIG}"'
62
+ ]
63
+ end
64
+
65
+ def token
66
+ ["token=$(curl -s --location --request POST \"https://#{opts[:gateway]}/auth\" --header \"Content-Type: application/x-www-form-urlencoded\" --data-urlencode \"username=#{opts[:user]}\" --data-urlencode \"password=#{opts[:password]}\" --data-urlencode \"token=true\")"] # rubocop:disable Layout/LineLength
67
+ end
68
+
69
+ def scan_start
70
+ [
71
+ 'while true; do ' \
72
+ "result=$(curl -s -o /dev/null -w ''%{http_code}'' --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id\" --header \"Authorization: Bearer $token\"); " + # rubocop:disable Layout/LineLength, Style/FormatStringToken
73
+ 'echo "Waiting for scan to start..."; ' \
74
+ 'echo " Result: ${result}"; ' \
75
+ 'if [ "${result}" = "200" ]; then break; fi; ' \
76
+ 'sleep 10; done'
77
+ ]
78
+ end
79
+
80
+ def scan_complete
81
+ [
82
+ 'while true; do ' \
83
+ "result=$(curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id\" --header \"Authorization: Bearer $token\" | jq -r '.scanStatus'); " + # rubocop:disable Layout/LineLength
84
+ 'echo "Waiting for scan to complete..."; ' \
85
+ 'echo " Result: ${result}"; ' \
86
+ 'if [ "${result}" = "SUCCESS" ]; then break; fi; ' \
87
+ 'sleep 10; done; sleep 30'
88
+ ]
89
+ end
90
+
91
+ def artifacts
92
+ [
93
+ "curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id/software\" --header \"Authorization: Bearer $token\" | jq . > software.json", # rubocop:disable Layout/LineLength
94
+ "curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id/vuln\" --header \"Authorization: Bearer $token\" | jq . > vulnerabilities.json" # rubocop:disable Layout/LineLength
95
+ ]
96
+ end
97
+
98
+ def severities
99
+ [
100
+ "response=$(curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id/vuln/count\" --header \"Authorization: Bearer $token\")", # rubocop:disable Layout/LineLength
101
+ 'severity5=$(jq -r ".severity5Count" <<< "${response}")',
102
+ 'severity4=$(jq -r ".severity4Count" <<< "${response}")'
103
+ ]
104
+ end
105
+
106
+ def outputs # rubocop:disable Metrics/MethodLength
107
+ [
108
+ 'if [ "$severity5" = "null" ]; then ' \
109
+ 'echo "ERROR: Wrong ImageID or problem during vulnerabilities count." >&2; ' \
110
+ 'exit 1; fi',
111
+ 'if [ "$severity4" = "null" ]; then ' \
112
+ 'echo "ERROR: Wrong ImageID or problem during vulnerabilities count." >&2; ' \
113
+ 'exit 1; fi',
114
+ 'echo "Severity5: $severity5, Severity4: $severity4"',
115
+ 'risk=$((($severity5*3)+($severity4)))',
116
+ 'echo "Risk: $risk"',
117
+ "if (($risk > \"#{opts[:acceptable_risk]}\")); then " \
118
+ 'echo "Too many vulnerabilities. Severity5: $severity5, Severity4: $severity4" >&2; ' \
119
+ 'exit 1; fi'
120
+ ]
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ # Pipedawg::Job::Qualys class
6
+ class Qualys < Job
7
+ def initialize(name, opts = {})
8
+ super name, opts
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ class Skopeo
6
+ # Pipedawg::Job::Skopeo::Copy class
7
+ class Copy < Job::Skopeo
8
+ def initialize(name, opts = {})
9
+ opts = {
10
+ config: {}, copy_image: name, destinations: [{ copy_image: nil, flags: [], options: {} }], flags: [],
11
+ logins: {}, options: {}, stage: '${CI_PROJECT_DIR}/stage', trusted_ca_cert_source_files: [],
12
+ trusted_ca_cert_target_file: '/etc/docker/certs.d/ca.crt'
13
+ }.merge(opts)
14
+ super name, opts
15
+ update
16
+ end
17
+
18
+ def update # rubocop:disable Metrics/AbcSize
19
+ require 'json'
20
+ opts[:script] = debug + config + cert_copies + login + mkstage + pull + (
21
+ opts[:destinations].map { |d| push(d) }
22
+ ).flatten(1)
23
+ end
24
+
25
+ private
26
+
27
+ def config
28
+ ['export CONFIG=$(mktemp -d)', "echo #{opts[:config].to_json.inspect} > \"${CONFIG}/config.json\""]
29
+ end
30
+
31
+ def cert_copies
32
+ ["mkdir -p $(dirname \"#{opts[:trusted_ca_cert_target_file]}\")"] +
33
+ Array(opts[:trusted_ca_cert_source_files]).map do |cert|
34
+ "cat \"#{cert}\" >> \"#{opts[:trusted_ca_cert_target_file]}\""
35
+ end
36
+ end
37
+
38
+ def login
39
+ opts.fetch(:logins, {}).map do |k, v|
40
+ "echo \"#{v['password']}\" | #{opts[:command]} login --authfile \"${CONFIG}/config.json\" --username \"#{v['username']}\" --password-stdin \"#{k}\"" # rubocop:disable Layout/LineLength
41
+ end
42
+ end
43
+
44
+ def mkstage
45
+ ["mkdir -p \"#{opts[:stage]}\""]
46
+ end
47
+
48
+ def pull
49
+ copy(opts, "docker://#{opts[:copy_image]}", "\"dir://#{opts[:stage]}\"")
50
+ end
51
+
52
+ def push(destination_opts)
53
+ copy(destination_opts, "\"dir://#{opts[:stage]}\"", "docker://#{destination_opts[:copy_image]}")
54
+ end
55
+
56
+ def copy(copy_opts, source, destination)
57
+ Array(["#{opts[:command]} copy --authfile \"${CONFIG}/config.json\"", flags(copy_opts), options(copy_opts),
58
+ source, destination].reject(&:empty?).join(' '))
59
+ end
60
+
61
+ def flags(opts)
62
+ opts.fetch(:flags, []).uniq.map { |f| "--#{f}" }.join(' ')
63
+ end
64
+
65
+ def options(opts)
66
+ opts.fetch(:options, {}).map { |k, v| "--#{k} \"#{v}\"" }.join(' ')
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipedawg
4
+ class Job
5
+ # Pipedawg::Job::Skopeo class
6
+ class Skopeo < Job
7
+ def initialize(name, opts = {})
8
+ opts = {
9
+ command: 'skopeo',
10
+ image: { entrypoint: [''], name: 'quay.io/skopeo/stable:latest' }
11
+ }.merge(opts)
12
+ super name, opts
13
+ end
14
+ end
15
+ end
16
+ end
data/lib/pipedawg/job.rb CHANGED
@@ -10,18 +10,31 @@ module Pipedawg
10
10
  @opts = {
11
11
  artifacts: {},
12
12
  cache: {},
13
+ debug: true,
13
14
  image: { name: 'ruby:2.5' },
14
15
  needs: [],
15
16
  retry: nil,
16
17
  rules: nil,
17
18
  script: [],
18
19
  stage: 'build',
19
- tags: []
20
+ tags: [],
21
+ variables: nil
20
22
  }.merge(opts)
21
23
  end
22
24
 
23
25
  def to_hash
24
- { "#{name}": opts.compact }
26
+ keys = %i[artifacts cache image needs retry rules script stage tags variables]
27
+ { "#{name}": opts.slice(*keys).compact }
28
+ end
29
+
30
+ private
31
+
32
+ def debug
33
+ if opts[:debug]
34
+ Pipedawg::Util.echo_proxy_vars
35
+ else
36
+ []
37
+ end
25
38
  end
26
39
  end
27
40
  end
data/lib/pipedawg/util.rb CHANGED
@@ -9,6 +9,7 @@ module Pipedawg
9
9
  item.map { |i| expand_env_vars(i) }
10
10
  when Hash
11
11
  item.each { |k, v| item[k] = expand_env_vars(v) }
12
+ item.transform_keys! { |k| expand_env_vars(k) }
12
13
  item
13
14
  when String
14
15
  item.gsub(/\${([^} ]+)}/) do |e|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pipedawg
4
- VERSION = '0.5.0'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/pipedawg.rb CHANGED
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pipedawg/job'
4
- require 'pipedawg/helm_copy_job'
5
- require 'pipedawg/kaniko_job'
4
+ require 'pipedawg/job/helm'
5
+ require 'pipedawg/job/helm/copy'
6
+ require 'pipedawg/job/kaniko'
7
+ require 'pipedawg/job/kaniko/build'
8
+ require 'pipedawg/job/qualys'
9
+ require 'pipedawg/job/qualys/scan'
10
+ require 'pipedawg/job/skopeo'
11
+ require 'pipedawg/job/skopeo/copy'
6
12
  require 'pipedawg/pipeline'
7
- require 'pipedawg/qualys_scan_job'
8
13
  require 'pipedawg/util'
9
14
  require 'pipedawg/version'
10
15
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipedawg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - harbottle
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-02-08 00:00:00.000000000 Z
11
+ date: 2022-03-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Generate GitLab CI pipelines.
14
14
  email:
@@ -20,11 +20,16 @@ files:
20
20
  - LICENSE.txt
21
21
  - README.md
22
22
  - lib/pipedawg.rb
23
- - lib/pipedawg/helm_copy_job.rb
24
23
  - lib/pipedawg/job.rb
25
- - lib/pipedawg/kaniko_job.rb
24
+ - lib/pipedawg/job/helm.rb
25
+ - lib/pipedawg/job/helm/copy.rb
26
+ - lib/pipedawg/job/kaniko.rb
27
+ - lib/pipedawg/job/kaniko/build.rb
28
+ - lib/pipedawg/job/qualys.rb
29
+ - lib/pipedawg/job/qualys/scan.rb
30
+ - lib/pipedawg/job/skopeo.rb
31
+ - lib/pipedawg/job/skopeo/copy.rb
26
32
  - lib/pipedawg/pipeline.rb
27
- - lib/pipedawg/qualys_scan_job.rb
28
33
  - lib/pipedawg/util.rb
29
34
  - lib/pipedawg/version.rb
30
35
  homepage: https://github.com/liger1978/pipedawg
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pipedawg
4
- # helm_copy_job class
5
- class HelmCopyJob < Job
6
- attr_accessor :helm_opts
7
-
8
- def initialize(name = 'build', opts = {}, helm_opts = {})
9
- @helm_opts = {
10
- chart: name, debug: true,
11
- destinations: [{ user: nil, password: nil, url: nil }],
12
- helm: 'helm',
13
- image: { entrypoint: [''], name: 'alpine/helm' },
14
- password: nil, url: nil, user: nil, version: nil
15
- }.merge(helm_opts)
16
- super name, opts
17
- update
18
- end
19
-
20
- def update # rubocop:disable Metrics/AbcSize
21
- opts[:image] = helm_opts[:image] if helm_opts[:image]
22
- opts[:script] = debug + pull + (helm_opts[:destinations].map { |d| push(d) }).flatten(1)
23
- end
24
-
25
- private
26
-
27
- def debug
28
- if helm_opts[:debug]
29
- Pipedawg::Util.echo_proxy_vars
30
- else
31
- []
32
- end
33
- end
34
-
35
- def pull
36
- case helm_opts[:url]
37
- when nil
38
- []
39
- when %r{^oci://}
40
- pull_oci
41
- else
42
- pull_classic
43
- end
44
- end
45
-
46
- def push(destination)
47
- case destination[:url]
48
- when nil
49
- []
50
- when %r{^oci://}
51
- push_oci(destination)
52
- else
53
- push_classic(destination)
54
- end
55
- end
56
-
57
- def pull_oci # rubocop:disable Metrics/AbcSize
58
- script = []
59
- if helm_opts[:url] && helm_opts[:chart] && helm_opts[:version]
60
- script = ['export HELM_EXPERIMENTAL_OCI=1']
61
- script << login_oci(helm_opts) if helm_opts[:user] && helm_opts[:password]
62
- script << "\"#{helm_opts[:helm]}\" pull \"#{helm_opts[:url]}/#{helm_opts[:chart]}\" --version \"#{helm_opts[:version]}\"" # rubocop:disable Layout/LineLength
63
- end
64
- script
65
- end
66
-
67
- def push_oci(destination) # rubocop:disable Metrics/AbcSize
68
- script = []
69
- if destination[:url] && helm_opts[:chart] && helm_opts[:version]
70
- script = ['export HELM_EXPERIMENTAL_OCI=1']
71
- script << login_oci(destination) if destination[:user] && destination[:password]
72
- script << "\"#{helm_opts[:helm]}\" push \"#{helm_opts[:chart]}-#{helm_opts[:version]}.tgz\" \"#{destination[:url]}\"" # rubocop:disable Layout/LineLength
73
- end
74
- script
75
- end
76
-
77
- def login_oci(login_opts)
78
- require 'uri'
79
- "echo \"#{login_opts[:password]}\" | \"#{helm_opts[:helm]}\" registry login --username \"#{login_opts[:user]}\" --password-stdin \"#{URI(login_opts[:url]).host}\"" # rubocop:disable Layout/LineLength
80
- end
81
-
82
- def pull_classic # rubocop:disable Metrics/AbcSize
83
- script = []
84
- if helm_opts[:url] && helm_opts[:chart] && helm_opts[:version]
85
- suffix = login_classic(helm_opts)
86
- script << "\"#{helm_opts[:helm]}\" repo add source \"#{helm_opts[:url]}\"#{suffix}"
87
- script << "\"#{helm_opts[:helm]}\" repo update"
88
- script << "\"#{helm_opts[:helm]}\" pull \"source/#{helm_opts[:chart]}\" --version \"#{helm_opts[:version]}\""
89
- end
90
- script
91
- end
92
-
93
- def push_classic(destination)
94
- script = []
95
- if destination[:url] && helm_opts[:chart] && helm_opts[:version]
96
- script << plugin_classic
97
- suffix = login_classic(destination)
98
- script << "\"#{helm_opts[:helm]}\" cm-push \"#{helm_opts[:chart]}-#{helm_opts[:version]}.tgz\" \"#{destination[:url]}\"#{suffix}" # rubocop:disable Layout/LineLength
99
- end
100
- script
101
- end
102
-
103
- def login_classic(login_opts)
104
- if login_opts[:user] && login_opts[:password]
105
- " --username \"#{login_opts[:user]}\" --password \"#{login_opts[:password]}\""
106
- else
107
- ''
108
- end
109
- end
110
-
111
- def plugin_classic
112
- "\"#{helm_opts[:helm]}\" plugin list | grep -q cm-push || \"#{helm_opts[:helm]}\" plugin install https://github.com/chartmuseum/helm-push"
113
- end
114
- end
115
- end
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pipedawg
4
- # kaniko_job class
5
- class KanikoJob < Job
6
- attr_accessor :kaniko_opts
7
-
8
- def initialize(name = 'build', opts = {}, kaniko_opts = {}) # rubocop:disable Metrics/MethodLength
9
- @kaniko_opts = {
10
- build_args: {},
11
- config: {
12
- '$CI_REGISTRY': { username: '$CI_REGISTRY_USER', password: '$CI_REGISTRY_PASSWORD' }
13
- },
14
- config_file: '/kaniko/.docker/config.json', context: '${CI_PROJECT_DIR}', debug: true,
15
- destinations: [], dockerfile: 'Dockerfile', executor: '/kaniko/executor', external_files: {},
16
- flags: [], ignore_paths: [], insecure_registries: [],
17
- image: { entrypoint: [''], name: 'gcr.io/kaniko-project/executor:debug' }, options: {},
18
- registry_certificates: {}, registry_mirrors: [], skip_tls_verify_registry: [],
19
- trusted_ca_cert_source_files: [], trusted_ca_cert_target_file: '/kaniko/ssl/certs/ca-certificates.crt'
20
- }.merge(kaniko_opts)
21
- super name, opts
22
- update
23
- end
24
-
25
- def update # rubocop:disable Metrics/AbcSize
26
- require 'json'
27
- opts[:image] = kaniko_opts[:image] if kaniko_opts[:image]
28
- opts[:script] = debug + config + cert_copies + file_copies + Array(kaniko_cmd)
29
- end
30
-
31
- private
32
-
33
- def debug
34
- if kaniko_opts[:debug]
35
- Pipedawg::Util.echo_proxy_vars
36
- else
37
- []
38
- end
39
- end
40
-
41
- def config
42
- ["echo #{kaniko_opts[:config].to_json.inspect} > \"#{kaniko_opts[:config_file]}\""]
43
- end
44
-
45
- def cert_copies
46
- Array(kaniko_opts[:trusted_ca_cert_source_files]).map do |cert|
47
- "cat \"#{cert}\" >> \"#{kaniko_opts[:trusted_ca_cert_target_file]}\""
48
- end
49
- end
50
-
51
- def file_copies
52
- kaniko_opts[:external_files].map do |source, dest|
53
- "cp \"#{source}\" \"#{kaniko_opts[:context]}/#{dest}\""
54
- end
55
- end
56
-
57
- def kaniko_cmd # rubocop:disable Metrics/AbcSize
58
- ["\"#{kaniko_opts[:executor]}\" --context \"#{kaniko_opts[:context]}\"",
59
- "--dockerfile \"#{kaniko_opts[:dockerfile]}\"", flags, options, build_args,
60
- ignore_paths, insecure_registries, registry_certificates, registry_mirrors,
61
- destinations, skip_tls_verify_registries].reject(&:empty?).join(' ')
62
- end
63
-
64
- def flags
65
- flags = kaniko_opts[:flags].clone
66
- flags << 'no-push' if kaniko_opts[:destinations].empty?
67
- flags.uniq.map { |f| "--#{f}" }.join(' ')
68
- end
69
-
70
- def options
71
- kaniko_opts[:options].map { |k, v| "--#{k}=\"#{v}\"" }.join(' ')
72
- end
73
-
74
- def build_args
75
- kaniko_opts[:build_args].map { |k, v| "--build-arg #{k}=\"#{v}\"" }.join(' ')
76
- end
77
-
78
- def ignore_paths
79
- Array(kaniko_opts[:ignore_paths]).map { |p| "--ignore-path #{p}" }.join(' ')
80
- end
81
-
82
- def insecure_registries
83
- Array(kaniko_opts[:insecure_registries]).map do |r|
84
- "--insecure-registry #{r}"
85
- end.join(' ')
86
- end
87
-
88
- def registry_certificates
89
- kaniko_opts[:registry_certificates].map do |k, v|
90
- "--registry-certificate #{k}=\"#{v}\""
91
- end.join(' ')
92
- end
93
-
94
- def registry_mirrors
95
- Array(kaniko_opts[:registry_mirrors]).map { |r| "--registry-mirror #{r}" }.join(' ')
96
- end
97
-
98
- def destinations
99
- kaniko_opts[:destinations].map { |d| "--destination #{d}" }.join(' ')
100
- end
101
-
102
- def skip_tls_verify_registries
103
- Array(kaniko_opts[:skip_tls_verify_registry]).map do |r|
104
- "--skip-tls-verify-registry #{r}"
105
- end.join(' ')
106
- end
107
- end
108
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Pipedawg
4
- # qualys_scan_job class
5
- class QualysScanJob < Job
6
- attr_accessor :qualys_opts
7
-
8
- def initialize(name = 'build', opts = {}, qualys_opts = {})
9
- @qualys_opts = {
10
- acceptable_risk: '${QUALYS_ACCEPTABLE_IMAGE_RISK}',
11
- artifacts: {
12
- expire_in: '1 month', paths: ['software.json', 'vulnerabilities.json'], when: 'always'
13
- }, debug: true, gateway: '${QUALYS_GATEWAY}', image: nil, password: '${QUALYS_PASSWORD}', rules: nil,
14
- scan_image: '${QUALYS_IMAGE}', scan_target_prefix: 'qualys_scan_target', tags: nil, user: '${QUALYS_USERNAME}',
15
- variables: { GIT_STRATEGY: 'clone' }
16
- }.merge(qualys_opts)
17
- super name, opts
18
- update
19
- end
20
-
21
- def update # rubocop:disable Metrics/AbcSize
22
- require 'json'
23
- opts[:artifacts] = qualys_opts[:artifacts] if qualys_opts[:artifacts]
24
- opts[:image] = qualys_opts[:image]
25
- opts[:rules] = qualys_opts[:rules] if qualys_opts[:rules]
26
- opts[:tags] = qualys_opts[:tags] if qualys_opts[:tags]
27
- opts[:variables] = qualys_opts[:variables] if qualys_opts[:variables]
28
- opts[:script] = debug + image + token + scan_start + scan_complete + artifacts + severities + outputs
29
- end
30
-
31
- private
32
-
33
- def debug # rubocop:disable Metrics/MethodLength
34
- if qualys_opts[:debug]
35
- Pipedawg::Util.echo_proxy_vars + [
36
- 'echo Qualys settings:',
37
- "echo Qualys gateway: \"#{qualys_opts[:gateway]}\"",
38
- "echo Qualys username: \"#{qualys_opts[:user]}\"",
39
- "if [ \"#{qualys_opts[:password]}\" != '' ]; then " \
40
- 'echo Qualys password is not empty; else ' \
41
- 'echo Qualys password is not set; exit 1; fi'
42
- ]
43
- else
44
- []
45
- end
46
- end
47
-
48
- def image
49
- [
50
- "image_target=\"#{qualys_opts[:scan_target_prefix]}:$(echo #{qualys_opts[:scan_image]} | sed 's/^[^/]*\\///'| sed 's/[:/]/-/g')\"", # rubocop:disable Layout/LineLength
51
- "docker pull \"#{qualys_opts[:scan_image]}\"",
52
- "docker image tag \"#{qualys_opts[:scan_image]}\" \"${image_target}\"",
53
- "image_id=$(docker inspect --format=\"{{index .Id}}\" \"#{qualys_opts[:scan_image]}\" | cut -c8-19)",
54
- 'echo "Image ID: ${image_id}"'
55
- ]
56
- end
57
-
58
- def token
59
- ["token=$(curl -s --location --request POST \"https://#{qualys_opts[:gateway]}/auth\" --header \"Content-Type: application/x-www-form-urlencoded\" --data-urlencode \"username=#{qualys_opts[:user]}\" --data-urlencode \"password=#{qualys_opts[:password]}\" --data-urlencode \"token=true\")"] # rubocop:disable Layout/LineLength
60
- end
61
-
62
- def scan_start
63
- [
64
- 'while true; do ' \
65
- "result=$(curl -s -o /dev/null -w ''%{http_code}'' --location --request GET \"https://#{qualys_opts[:gateway]}/csapi/v1.2/images/$image_id\" --header \"Authorization: Bearer $token\"); " + # rubocop:disable Layout/LineLength, Style/FormatStringToken
66
- 'echo "Waiting for scan to start..."; ' \
67
- 'echo " Result: ${result}"; ' \
68
- 'if [ "${result}" = "200" ]; then break; fi; ' \
69
- 'sleep 10; done'
70
- ]
71
- end
72
-
73
- def scan_complete
74
- [
75
- 'while true; do ' \
76
- "result=$(curl -s --location --request GET \"https://#{qualys_opts[:gateway]}/csapi/v1.2/images/$image_id\" --header \"Authorization: Bearer $token\" | jq -r '.scanStatus'); " + # rubocop:disable Layout/LineLength
77
- 'echo "Waiting for scan to complete..."; ' \
78
- 'echo " Result: ${result}"; ' \
79
- 'if [ "${result}" = "SUCCESS" ]; then break; fi; ' \
80
- 'sleep 10; done; sleep 30'
81
- ]
82
- end
83
-
84
- def artifacts
85
- [
86
- "curl -s --location --request GET \"https://#{qualys_opts[:gateway]}/csapi/v1.2/images/$image_id/software\" --header \"Authorization: Bearer $token\" | jq . > software.json", # rubocop:disable Layout/LineLength
87
- "curl -s --location --request GET \"https://#{qualys_opts[:gateway]}/csapi/v1.2/images/$image_id/vuln\" --header \"Authorization: Bearer $token\" | jq . > vulnerabilities.json" # rubocop:disable Layout/LineLength
88
- ]
89
- end
90
-
91
- def severities
92
- [
93
- "response=$(curl -s --location --request GET \"https://#{qualys_opts[:gateway]}/csapi/v1.2/images/$image_id/vuln/count\" --header \"Authorization: Bearer $token\")", # rubocop:disable Layout/LineLength
94
- 'severity5=$(jq -r ".severity5Count" <<< "${response}")',
95
- 'severity4=$(jq -r ".severity4Count" <<< "${response}")'
96
- ]
97
- end
98
-
99
- def outputs # rubocop:disable Metrics/MethodLength
100
- [
101
- 'if [ "$severity5" = "null" ]; then ' \
102
- 'echo "ERROR: Wrong ImageID or problem during vulnerabilities count." >&2; ' \
103
- 'exit 1; fi',
104
- 'if [ "$severity4" = "null" ]; then ' \
105
- 'echo "ERROR: Wrong ImageID or problem during vulnerabilities count." >&2; ' \
106
- 'exit 1; fi',
107
- 'echo "Severity5: $severity5, Severity4: $severity4"',
108
- 'risk=$((($severity5*3)+($severity4)))',
109
- 'echo "Risk: $risk"',
110
- "if (($risk > \"#{qualys_opts[:acceptable_risk]}\")); then " \
111
- 'echo "Too many vulnerabilities. Severity5: $severity5, Severity4: $severity4" >&2; ' \
112
- 'exit 1; fi'
113
- ]
114
- end
115
- end
116
- end