talos 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6490791a8be853eea69f1bd18fdc18921ab76a62
4
- data.tar.gz: 62cc19aa2882f7ec979f381058fdbb2a72fbc7a9
3
+ metadata.gz: a963d1d621605ff8c6bbc9fcd5a66ec02f63509c
4
+ data.tar.gz: a16ebee4fbc780bf4911e6df1eeab33701f35e45
5
5
  SHA512:
6
- metadata.gz: b620929ce16fb6f872660a250b88f583a4b5ddd965a47b4b9d31372331f6a71c06b52cc0b6160b0677d5a6d1fcbca43e4c537cc3889791fd6a834831f8b18c51
7
- data.tar.gz: db70975dbeac2c5a8461b9761cfbaca65db63a0c68d4f4089e8cf52525cd7dd7497b2b6adecc1bb82436da2e2eaaa410fedebb6f66ac9c409b63ea2e0440c5c3
6
+ metadata.gz: 32b2795e08276905aec04ee7918da2041a612424772df39c65fe86ae961bea50cc44a3a53ab76fba03e2eed92bb7cc8dc84962dd79a9c3d55374b87ef6e023b4
7
+ data.tar.gz: c77f95c6f9c96a8cc25ba202548dda2be9d0495407106a307f380ce5edf484b1feb141dbb294981023011c4dcd447edabdf196df100baff91d8783f4bdf56303
@@ -0,0 +1,177 @@
1
+ Talos
2
+ =====
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/talos.svg)](http://badge.fury.io/rb/talos)
5
+ [![Build Status](https://travis-ci.org/spotify/talos.png?branch=master)](https://travis-ci.org/spotify/talos)
6
+
7
+ Talos is a rack application which servers Hiera yaml files over HTTP.
8
+ It authorizes clients based on the SSL certificates issued by the Puppet CA and returns only the files in the Hiera scope.
9
+
10
+ Talos is used to store and distribute secrets via Hiera to the masterless puppet clients.
11
+
12
+ How it works
13
+ ------------
14
+ Talos listens for incoming HTTP requests and returns compressed hiera
15
+ tree based on the client's SSL certificate.
16
+
17
+ To determine the list of files to send, Talos matches the certificate
18
+ common name against a list of regular expressions.
19
+
20
+ Fetching the tree
21
+ -----------------
22
+
23
+ It's possible to run a cron task or create a wrapper around the puppet
24
+ agent. Here's an example of the client-side code which uses local puppet SSL key
25
+ to authenticate:
26
+
27
+ ```ruby
28
+ require 'puppet'
29
+ Puppet[:confdir] = '/etc/puppetlabs/puppet/'
30
+ `/usr/bin/curl -s --fail -X GET -k https://talos.internal}/ \
31
+ --cert #{Puppet[:hostcert]} --key #{Puppet[:hostprivkey]} \
32
+ --data-urlencode pool=#{Facter.value(:pool)} > /etc/talos/tree.tar.gz`
33
+ `/bin/tar xzf /etc/talos/tree.tar.gz -C /etc/talos/hiera_secrets`
34
+ ```
35
+
36
+ In this example the client also passes `pool` variable which will
37
+ be included in the Hiera scope if `unsafe_scopes` option is enabled.
38
+
39
+ The received copy of the tree could be included in the local hiera config
40
+ and used in the normal puppet runs.
41
+
42
+ Configuration
43
+ -------------
44
+ Talos configuration is stored in `/etc/talos/talos.yaml`:
45
+
46
+ ```yaml
47
+ scopes:
48
+ # lon-puppet-a1: site = lon, role = puppet, pool = a
49
+ - match: '(?<site>[[:alpha:]]+)-(?<role>[a-z0-9]+)-(?<pool>[[:alpha:]]+)'
50
+ facts:
51
+ environment: production
52
+ - match: 'cloud\.example\.com'
53
+ facts:
54
+ environment: testing
55
+
56
+ unsafe_scopes: true
57
+ ```
58
+
59
+ When receiving a request, Talos iterates over `scopes` list and matches
60
+ the client certificate against the `match` blocks. If the match is
61
+ successful, Talos does 2 things:
62
+
63
+ 1. Adds all the named captures from the regexp to the Hiera scope
64
+ 2. Adds all the `facts` to the Hiera scope
65
+
66
+ Talos will iterate over all the regexps updating the
67
+ Hiera scope, meaning that the later matches will override the existing
68
+ scope on collision.
69
+
70
+ If `unsafe_scopes` option is enabled, Talos will also add all the parameters
71
+ passed by the client to the Hiera scope.
72
+
73
+ Hiera
74
+ -----
75
+ You need to provide `/etc/talos/hiera.yaml` file to configure Hiera
76
+ backend on the Talos server:
77
+
78
+ ```yaml
79
+ ---
80
+ :backends:
81
+ - yaml
82
+ :hierarchy:
83
+ - 'hiera-secrets/fqdn/%{fqdn}'
84
+ - 'hiera-secrets/role/%{role}/%{pod}/%{pool}'
85
+ - 'hiera-secrets/role/%{role}/%{pod}'
86
+ - 'hiera-secrets/role/%{role}'
87
+ - 'hiera-secrets/pod/%{pod}'
88
+ - 'hiera-secrets/common'
89
+ :yaml:
90
+ :datadir: '/etc/puppet'
91
+ :merge_behavior: :deeper
92
+ ```
93
+
94
+ Talos will use the `datadir` option to search for YAML files and it
95
+ will return only the files that match the Hiera scope of the clients.
96
+
97
+
98
+ Installing
99
+ ----------
100
+
101
+ First, install talos using rubygems:
102
+
103
+ $ gem install talos
104
+
105
+ Create a separate user and Document Root for the Rack application:
106
+
107
+ $ useradd talos --system --create-home --home-dir /var/lib/talos
108
+ $ mkdir -p /var/lib/talos/public /var/lib/talos/tmp /etc/talos
109
+ $ chown -R talos:talos /var/lib/talos/ /etc/talos
110
+
111
+ Then copy [config.ru](config.ru) to `/var/lib/talos/` directory.
112
+
113
+ You also need to copy and adjust [hiera.yaml](spec/fixtures/hiera.yaml) and
114
+ [talos.yaml](spec/fixtures/talos.yaml) configs in `/etc/talos` directory.
115
+
116
+ ### Hiera repository
117
+
118
+ You need to have a copy of the hiera-secrets repository available on the
119
+ talos server. Make sure it's located at the `datadir` specified in
120
+ `/etc/talos/hiera.yaml`
121
+
122
+ ### Apache
123
+
124
+ You can run Talos using Passenger or any other application server. Make
125
+ sure you use Puppet SSL keys to validate the client certificates and to
126
+ forward `SSL_CLIENT_S_DN_CN` header:
127
+
128
+ ```
129
+ <VirtualHost *:443>
130
+ DocumentRoot "/var/lib/talos/public"
131
+
132
+ <Directory "/var/lib/talos/public">
133
+ Require all granted
134
+ </Directory>
135
+
136
+ SSLEngine on
137
+ SSLCertificateFile "/etc/puppetlabs/puppet/ssl/certs/talos.internal.pem"
138
+ SSLCertificateKeyFile "/etc/puppetlabs/puppet/ssl/private_keys/talos.internal.pem"
139
+ SSLCertificateChainFile "/etc/puppetlabs/puppet/ssl/certs/ca.pem"
140
+ SSLCACertificatePath "/etc/ssl/certs"
141
+ SSLCACertificateFile "/etc/puppetlabs/puppet/ssl/certs/ca.pem"
142
+ SSLCARevocationFile "/etc/puppetlabs/puppet/ssl/crl.pem"
143
+ SSLVerifyClient require
144
+ SSLOptions +StdEnvVars +FakeBasicAuth
145
+ RequestHeader set SSL_CLIENT_S_DN_CN "%{SSL_CLIENT_S_DN_CN}s"
146
+ </VirtualHost>
147
+ ```
148
+
149
+ Contributing
150
+ ------------
151
+ 1. Fork the project on github
152
+ 2. Create your feature branch
153
+ 3. Open a Pull Request
154
+
155
+ This project adheres to the [Open Code of Conduct][code-of-conduct]. By
156
+ participating, you are expected to honor this code.
157
+
158
+ [code-of-conduct]:
159
+ https://github.com/spotify/code-of-conduct/blob/master/code-of-conduct.md
160
+
161
+ License
162
+ -----------------
163
+ ```text
164
+ Copyright 2013-2016 Spotify AB
165
+
166
+ Licensed under the Apache License, Version 2.0 (the "License");
167
+ you may not use this file except in compliance with the License.
168
+ You may obtain a copy of the License at
169
+
170
+ http://www.apache.org/licenses/LICENSE-2.0
171
+
172
+ Unless required by applicable law or agreed to in writing, software
173
+ distributed under the License is distributed on an "AS IS" BASIS,
174
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
175
+ See the License for the specific language governing permissions and
176
+ limitations under the License.
177
+ ```
data/config.ru CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rubygems'
2
2
  # uncomment if you wish to run from source code
3
- libdir = File.join(File.dirname(__FILE__), 'lib')
4
- $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+ # libdir = File.join(File.dirname(__FILE__), 'lib')
4
+ # $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
5
5
  require 'talos'
6
6
  run Talos
@@ -1,4 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
+ #--
3
+ # Copyright 2015 Spotify AB
4
+ #
5
+ # The contents of this file are licensed under the Apache License, Version 2.0
6
+ # (the "License"); you may not use this file except in compliance with the
7
+ # License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations under
15
+ # the License.
16
+ #++
17
+
2
18
  require 'sinatra/base'
3
19
  require 'json'
4
20
  require 'hiera'
@@ -8,91 +24,89 @@ require 'archive/tar/minitar'
8
24
  require 'pathname'
9
25
  include Archive::Tar
10
26
 
11
- def get_scope(fqdn, role = nil, pod = nil, pool = nil)
12
- # FQDN from ssl
13
- # role FQDN or client if unavailable
14
- # pod - FQDN or client
15
- # site - from pod
16
- # spenvironment - FQDN
17
- # pool - hostname or default to a
18
- scope = { 'fqdn' => fqdn }
19
- result = /(([[:alpha:]]+)\d*)-([a-z0-9]+)-([[:alpha:]])+/.match(fqdn)
20
-
21
- if !result.nil?
22
- scope['pod'] = result.captures[0]
23
- scope['site'] = result.captures[1]
24
- scope['role'] = result.captures[2]
25
- scope['pool'] = result.captures[3]
26
- scope['site'] = scope['pod'].tr('[0-9]', '')
27
- else
28
- # FQDN is not following our naming standard
29
- scope['pod'] = pod
30
- scope['role'] = role
31
- scope['pool'] = pool
32
- scope['site'] = scope['pod'].tr('[0-9]', '') unless pod.nil?
33
- end
34
-
35
- scope
36
- end
37
-
38
-
39
27
  class Talos < Sinatra::Base
40
- def absolute_datadir
41
- datadir = settings.hiera[:yaml][:datadir]
42
- datadir = File.join(File.dirname(__FILE__), '..', datadir) if Pathname.new(datadir).relative?
28
+ def self.prepare_config(path)
29
+ set :talos, YAML.load_file(path)
30
+ settings.talos['scopes'].each do |scope_config|
31
+ begin
32
+ scope_config['regexp'] = Regexp.new(scope_config['match'])
33
+ rescue
34
+ fail "Invalid regexp: #{scope_config['match']}"
35
+ end
36
+ end
43
37
  end
44
38
 
45
39
  configure :development do
46
40
  require 'sinatra/reloader'
47
41
  register Sinatra::Reloader
48
42
  set :hiera, Hiera::Config::load(File.expand_path('spec/fixtures/hiera.yaml'))
43
+ prepare_config('spec/fixtures/talos.yaml')
49
44
  set :show_exceptions, false
50
45
  end
46
+
51
47
  configure :production do
52
48
  set :hiera, Hiera::Config::load(File.expand_path('/etc/talos/hiera.yaml'))
49
+ prepare_config('/etc/talos/talos.yaml')
50
+ warn("SECURITY WARNING: unsafe_scopes are enabled, SSL authentication bypass is possible") if settings.talos['unsafe_scopes']
53
51
  end
54
52
 
55
- get '/' do
56
- fqdn = settings.development? ? params[:fqdn] : request.env['HTTP_SSL_CLIENT_S_DN_CN']
53
+ def absolute_datadir
54
+ datadir = settings.hiera[:yaml][:datadir]
55
+ datadir = File.join(File.dirname(__FILE__), '..', datadir) if Pathname.new(datadir).relative?
56
+ end
57
57
 
58
- # Get role/pod/pool from GET data(for hosts that don't follow our naming standard)
59
- unless params[:role].nil?
60
- role = params[:role].empty? ? nil : params[:role]
61
- end
62
- unless params[:pod].nil?
63
- pod = params[:pod].empty? ? nil : params[:pod]
64
- end
65
- unless params[:pool].nil?
66
- pool = params[:pool].empty? ? nil : params[:pool]
58
+ # Extracts scopes from FQDN using regexp with named captures
59
+ # Falls back to insecure arguments passed by the puppet agent
60
+ # (needed for the hosts not following naming convention)
61
+ def get_scope(fqdn)
62
+ scope = {'fqdn' => fqdn}
63
+ settings.talos['scopes'].each do | scope_config|
64
+ if m = fqdn.match(scope_config['regexp'])
65
+ scope.update(Hash[ m.names.zip( m.captures ) ])
66
+ scope.update(scope_config['facts'])
67
+ end
67
68
  end
68
69
 
69
- scope = get_scope(fqdn, role, pod, pool)
70
- files_to_pack = []
70
+ unsafe_scope = settings.talos['unsafe_scopes'] ? request.env['rack.request.query_hash'] : {}
71
+ scope.update(unsafe_scope)
72
+ # scope = {"pod"=>"lon3", "site"=>"lon", "role"=>"puppet", "pool"=>"a"}
73
+ scope
74
+ end
71
75
 
72
- # Get all yaml files matching scope
76
+ def files_in_scope(scope)
77
+ files = []
73
78
  Hiera::Backend.datasources(scope, nil) do |source, yamlfile|
74
79
  yamlfile = Hiera::Backend.datafile(:yaml, scope, source, 'yaml') || next
75
80
  next unless File.exist?(yamlfile)
76
81
  # Strip path from filename
77
- files_to_pack << yamlfile.gsub(settings.hiera[:yaml][:datadir] + '/', '')
82
+ files << yamlfile.gsub(settings.hiera[:yaml][:datadir] + '/', '')
78
83
  end
84
+ files
85
+ end
79
86
 
87
+ def compress_files(files)
80
88
  output = StringIO.new
81
89
  begin
82
90
  sgz = Zlib::GzipWriter.new(output)
83
91
  tar = Minitar::Output.new(sgz)
84
-
85
- Dir.chdir(absolute_datadir) do
86
- files_to_pack.each { |f| Minitar.pack_file(f, tar) }
87
- end
92
+ Dir.chdir(absolute_datadir) { files.each { |f| Minitar.pack_file(f, tar) } }
88
93
  ensure
89
94
  tar.close
90
95
  end
96
+ output
97
+ end
98
+
99
+ get '/' do
100
+ fqdn = settings.development? ? params[:fqdn] : request.env['HTTP_SSL_CLIENT_S_DN_CN']
101
+ scope = get_scope(fqdn)
102
+ files_to_pack = files_in_scope(scope)
103
+ archive = compress_files(files_to_pack)
91
104
  content_type 'application/x-gzip'
92
- output.string
105
+ archive.string
93
106
  end
94
107
 
95
108
  # Get the checksum the data folder symlink to
109
+ # Internal API
96
110
  get '/status' do
97
111
  begin
98
112
  File.readlink(absolute_datadir).split('.').last
@@ -4,15 +4,15 @@
4
4
 
5
5
  :hierarchy:
6
6
  - 'fqdn/%{fqdn}'
7
- - 'role/%{role}/%{spenvironment}/%{pool}'
7
+ - 'role/%{role}/%{environment}/%{pool}'
8
8
  - 'role/%{role}/%{pool}'
9
- - 'role/%{role}/%{spenvironment}'
9
+ - 'role/%{role}/%{environment}'
10
10
  - 'role/%{role}'
11
11
  - 'lsbdistcodename/%{lsbdistcodename}'
12
12
  - 'domain/%{domain}'
13
13
  - 'pod/%{pod}'
14
14
  - 'site/%{site}'
15
- - 'spenvironment/%{spenvironment}'
15
+ - 'environment/%{environment}'
16
16
  - common
17
17
 
18
18
  :yaml:
@@ -0,0 +1,10 @@
1
+ unsafe_scopes: true
2
+
3
+ scopes:
4
+ # lon3-puppet-a1: pod = lon3, site = lon3, role = puppet, pool = a
5
+ - match: '(?<pod>(?<site>[[:alpha:]]+)\d*)-(?<role>[a-z0-9]+)-(?<pool>[[:alpha:]]+)\d+'
6
+ facts:
7
+ environment: production
8
+ - match: 'cloud\.example\.com'
9
+ facts:
10
+ environment: testing
@@ -5,26 +5,30 @@ describe 'talos' do
5
5
 
6
6
  def match_query_to_files(query, files)
7
7
  get query
8
- last_response.should be_ok
9
- file = Tempfile.new('spec')
10
- file.write(last_response.body)
11
- file.flush
12
- output = `tar -tf #{file.path}`
13
- file.unlink
14
- files.each { |f| expect(output).to match(f) }
8
+ expect(last_response).to be_ok
9
+ Tempfile.open('spec') do |file|
10
+ file.write(last_response.body)
11
+ file.flush
12
+ files_in_archive = `tar -tf #{file.path}`.split
13
+ files.each { |f| expect(files.sort).to eq(files_in_archive.sort) }
14
+ end
15
15
  end
16
16
 
17
- it 'should return common.yaml' do
17
+ it 'should detect scope and return YAML files' do
18
18
  { '/?fqdn=testing.int.sto.example.com' =>
19
- ['common.yaml'],
19
+ %w(common.yaml),
20
20
  '/?role=puppet&pod=sto3&fqdn=sto3-puppet-a1.sto3.example.com' =>
21
- ['common.yaml', 'role/puppet.yaml'],
21
+ %w(common.yaml role/puppet.yaml),
22
22
  '/?fqdn=sto3-puppet-a1.sto3.example.com' =>
23
- ['common.yaml', 'role/puppet.yaml'],
23
+ %w(common.yaml role/puppet.yaml),
24
24
  '/?role=puppet&pod=sto3&fqdn=foo.bar' =>
25
25
  %w(common.yaml role/puppet.yaml fqdn/foo.bar.yaml),
26
26
  '/?fqdn=something.random&role=foobar&pool=z' =>
27
- %w(common.yaml role/foobar/z.yaml)
27
+ %w(common.yaml role/foobar/z.yaml),
28
+ '/?fqdn=sjc1-puppet-a1' =>
29
+ %w(common.yaml role/puppet.yaml site/sjc.yaml),
30
+ '/?fqdn=sjc1-foobar-a1.cloud.example.com' =>
31
+ %w(common.yaml site/sjc.yaml role/foobar/testing.yaml),
28
32
  }.each do |query, files|
29
33
  match_query_to_files(query, files)
30
34
  end
@@ -32,7 +36,7 @@ describe 'talos' do
32
36
 
33
37
  it 'should resturn the checksum master symlink to' do
34
38
  get '/status'
35
- last_response.should be_ok
36
- last_response.body.should match('3fa3fd97848a72ae539b75bccd6028cd1d4e92e3')
39
+ expect(last_response).to be_ok
40
+ expect(last_response.body).to match('3fa3fd97848a72ae539b75bccd6028cd1d4e92e3')
37
41
  end
38
42
  end
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
- s.version = '0.1.0'
2
+ s.version = '0.1.1'
3
3
  s.name = 'talos'
4
4
  s.authors = ['Alexey Lapitsky', 'Johan Haals']
5
5
  s.email = 'alexey@spotify.com'
6
- s.summary = %q{Distribute compressed hiera yaml files over HTTP}
7
- s.description = %q{Serve hiera data over HTTP}
6
+ s.summary = %q{Hiera secrets distribution over HTTP}
7
+ s.description = %q{Distribute compressed hiera yaml files to authenticated puppet clients over HTTP}
8
8
  s.homepage = 'https://github.com/spotify/talos'
9
9
  s.license = 'Apache 2.0'
10
10
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: talos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Lapitsky
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-12-17 00:00:00.000000000 Z
12
+ date: 2016-01-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -109,7 +109,8 @@ dependencies:
109
109
  - - '>='
110
110
  - !ruby/object:Gem::Version
111
111
  version: '2.9'
112
- description: Serve hiera data over HTTP
112
+ description: Distribute compressed hiera yaml files to authenticated puppet clients
113
+ over HTTP
113
114
  email: alexey@spotify.com
114
115
  executables: []
115
116
  extensions: []
@@ -119,14 +120,18 @@ files:
119
120
  - .rspec
120
121
  - Gemfile
121
122
  - LICENSE
123
+ - README.md
122
124
  - Rakefile
123
125
  - config.ru
124
126
  - lib/talos.rb
125
127
  - spec/fixtures/hiera.yaml
126
128
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/common.yaml
127
129
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/fqdn/foo.bar.yaml
130
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/testing.yaml
128
131
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/z.yaml
129
132
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/puppet.yaml
133
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/site/sjc.yaml
134
+ - spec/fixtures/talos.yaml
130
135
  - spec/spec_helper.rb
131
136
  - spec/talos_spec.rb
132
137
  - talos.gemspec
@@ -153,12 +158,15 @@ rubyforge_project:
153
158
  rubygems_version: 2.0.14
154
159
  signing_key:
155
160
  specification_version: 4
156
- summary: Distribute compressed hiera yaml files over HTTP
161
+ summary: Hiera secrets distribution over HTTP
157
162
  test_files:
158
163
  - spec/fixtures/hiera.yaml
159
164
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/common.yaml
160
165
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/fqdn/foo.bar.yaml
166
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/testing.yaml
161
167
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/foobar/z.yaml
162
168
  - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/role/puppet.yaml
169
+ - spec/fixtures/master.3fa3fd97848a72ae539b75bccd6028cd1d4e92e3/site/sjc.yaml
170
+ - spec/fixtures/talos.yaml
163
171
  - spec/spec_helper.rb
164
172
  - spec/talos_spec.rb