polyn-cli 0.1.1 → 0.1.4

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
  SHA256:
3
- metadata.gz: e6f27c81897f9c1970d3b228466c1f0b5f34b5f6c5c1286064e06ff7106e72e7
4
- data.tar.gz: f4aadba2632bfbc52aa066ac30f627de3a82af17651c1777e4cb4c07275ca967
3
+ metadata.gz: a516676ead604b6dcfc242c75addef95ad9512f4c7a9b009369cb46d581f946e
4
+ data.tar.gz: 9ebdfaec0ba3df014121d75d9903ee234dad27ba4444d45178cdf66fd132336d
5
5
  SHA512:
6
- metadata.gz: 6dfd33b774a154d24d54ccc63843633343832ca536a0feb69f677b8373bccebd6d4fde76fffccea26e1387644667da904999b1f4058672a89dfd89fd9b44a7ee
7
- data.tar.gz: 5744ef8f8f8db2e72a61b087f96c49db372217aee7964860485d5cb6aec65a4772ffc9a0f19e1e0f9fe2ae2eef0df9e63fa684b2408479e09939f1a2a53af2fe
6
+ metadata.gz: 545df32cdd3d928022e23798cbb08ee88521fc54248a1fd1911e80b88ff71d97987f092b2d73429942ec2fa66e818a8c0d88d11de30d7f5f77bd73ff3078e496
7
+ data.tar.gz: 58f0c9aaa9348efdf8c4310c1c1e7376cfb57d43e2f662015cbc224d14df0306ffc6da439f0618a079d08f57aeb3b08391b61cfa622ea5d1f8843fbd07989623
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyn-cli (0.1.1)
4
+ polyn-cli (0.1.4)
5
5
  dotenv (~> 2.7.6)
6
6
  json_schemer (~> 0.2)
7
7
  nats-pure (~> 2.0.0)
data/README.md CHANGED
@@ -33,6 +33,7 @@ Run `polyn up` to update your NATS server with the latest configuration in your
33
33
  ## Environment Variables
34
34
 
35
35
  * `NATS_SERVERS` - locations of your servers (defaults to localhost)
36
+ * `NATS_CREDENTIALS` - path to nats credentials file
36
37
  * `POLYN_ENV` - type of environment (defaults to "development")
37
38
 
38
39
  ## Development
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+
3
+ # We want to keep these commands out of the Docker image
4
+ # build and instead defer them till the container is run.
5
+ # terraform init may require credentials for a remote backend
6
+ # and we don't want that in the image for security reasons.
7
+ bundle exec polyn tf_init
8
+ bundle exec polyn up
@@ -5,11 +5,12 @@ module Polyn
5
5
  ##
6
6
  # Configuration data for Polyn::Cli
7
7
  class Configuration
8
- attr_reader :polyn_env, :nats_servers
8
+ attr_reader :polyn_env, :nats_servers, :nats_credentials
9
9
 
10
10
  def initialize
11
- @polyn_env = ENV["POLYN_ENV"] || "development"
12
- @nats_servers = ENV["NATS_SERVERS"] || "localhost:4222"
11
+ @polyn_env = ENV["POLYN_ENV"] || "development"
12
+ @nats_servers = ENV["NATS_SERVERS"] || "localhost:4222"
13
+ @nats_credentials = ENV["NATS_CREDENTIALS"] || ""
13
14
  end
14
15
  end
15
16
  end
@@ -14,21 +14,32 @@ module Polyn
14
14
 
15
15
  source_root File.join(File.expand_path(__dir__), "../templates")
16
16
 
17
+ def type
18
+ @type ||= event_type.split("/").last
19
+ end
20
+
21
+ def subdir
22
+ @subdir ||= begin
23
+ split = event_type.split("/") - [type]
24
+ split.join("/")
25
+ end
26
+ end
27
+
17
28
  def check_name
18
- Polyn::Cli::Naming.validate_event_type!(event_type)
29
+ Polyn::Cli::Naming.validate_event_type!(type)
19
30
  end
20
31
 
21
32
  def file_name
22
- @file_name ||= "#{event_type}.json"
33
+ @file_name ||= File.join(subdir, "#{type}.json")
23
34
  end
24
35
 
25
36
  def schema_id
26
- Polyn::Cli::Naming.dot_to_colon(event_type)
37
+ Polyn::Cli::Naming.dot_to_colon(type)
27
38
  end
28
39
 
29
40
  def create
30
- say "Creating new schema for #{event_type}"
31
- template "generators/schema.json", File.join(options.dir, "events/#{event_type}.json")
41
+ say "Creating new schema for #{file_name}"
42
+ template "generators/schema.json", File.join(options.dir, "events/#{file_name}")
32
43
  end
33
44
  end
34
45
  end
@@ -19,29 +19,44 @@ module Polyn
19
19
  def initialize(thor, **opts)
20
20
  @thor = thor
21
21
  @client = NATS.connect(Polyn::Cli.configuration.nats_servers).jetstream
22
- @bucket = client.key_value(opts.fetch(:store_name, STORE_NAME))
22
+ @store_name = opts.fetch(:store_name, STORE_NAME)
23
+ @bucket = client.key_value(@store_name)
23
24
  @cloud_event_schema = Polyn::Cli::CloudEvent.to_h.freeze
24
25
  @events_dir = opts.fetch(:events_dir, File.join(Dir.pwd, "events"))
25
26
  @events = {}
27
+ @existing_events = {}
26
28
  end
27
29
 
28
30
  def load_events
29
31
  thor.say "Loading events into the Polyn event registry from '#{events_dir}'"
30
32
  read_events
33
+ load_existing_events
31
34
 
32
35
  events.each do |name, event|
33
36
  bucket.put(name, JSON.generate(event))
34
37
  end
35
38
 
39
+ delete_missing_events
40
+
36
41
  true
37
42
  end
38
43
 
39
44
  private
40
45
 
41
- attr_reader :thor, :events, :client, :bucket, :cloud_event_schema, :events_dir
46
+ attr_reader :thor,
47
+ :events,
48
+ :client,
49
+ :bucket,
50
+ :cloud_event_schema,
51
+ :events_dir,
52
+ :store_name,
53
+ :existing_events
42
54
 
43
55
  def read_events
44
- Dir.glob(File.join(events_dir, "*.json")).each do |event_file|
56
+ event_files = Dir.glob(File.join(events_dir, "/**/*.json"))
57
+ validate_unique_event_types!(event_files)
58
+
59
+ event_files.each do |event_file|
45
60
  thor.say "Loading 'event #{event_file}'"
46
61
  data_schema = JSON.parse(File.read(event_file))
47
62
  event_type = File.basename(event_file, ".json")
@@ -53,6 +68,30 @@ module Polyn
53
68
  end
54
69
  end
55
70
 
71
+ def validate_unique_event_types!(event_files)
72
+ duplicates = find_duplicates(event_files)
73
+ unless duplicates.empty?
74
+ messages = duplicates.reduce([]) do |memo, (event_type, files)|
75
+ memo << [event_type, *files].join("\n")
76
+ end
77
+ message = [
78
+ "There can only be one of each event type. The following events were duplicated:",
79
+ *messages,
80
+ ].join("\n")
81
+ raise Polyn::Cli::ValidationError, message
82
+ end
83
+ end
84
+
85
+ def find_duplicates(event_files)
86
+ event_types = event_files.group_by do |event_file|
87
+ File.basename(event_file, ".json")
88
+ end
89
+ event_types.each_with_object({}) do |(event_type, files), hash|
90
+ hash[event_type] = files if files.length > 1
91
+ hash
92
+ end
93
+ end
94
+
56
95
  def validate_schema!(event_type, schema)
57
96
  JSONSchemer.schema(schema)
58
97
  rescue StandardError => e
@@ -68,6 +107,32 @@ module Polyn
68
107
  }),
69
108
  })
70
109
  end
110
+
111
+ def load_existing_events
112
+ sub = client.subscribe("#{key_prefix}.>")
113
+
114
+ loop do
115
+ msg = sub.next_msg
116
+ existing_events[msg.subject.gsub("#{key_prefix}.", "")] = msg.data
117
+ # A timeout is the only mechanism given to indicate there are no
118
+ # more messages
119
+ rescue NATS::IO::Timeout
120
+ break
121
+ end
122
+ sub.unsubscribe
123
+ end
124
+
125
+ def key_prefix
126
+ "$KV.#{store_name}"
127
+ end
128
+
129
+ def delete_missing_events
130
+ missing_events = existing_events.keys - events.keys
131
+ missing_events.each do |event|
132
+ thor.say "Deleting event #{event}"
133
+ bucket.delete(event)
134
+ end
135
+ end
71
136
  end
72
137
  end
73
138
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Polyn
4
4
  class Cli
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.4"
6
6
  end
7
7
  end
data/lib/polyn/cli.rb CHANGED
@@ -54,8 +54,11 @@ module Polyn
54
54
  directory "tf", File.join(options.dir, "tf")
55
55
  directory "events", File.join(options.dir, "events")
56
56
  template "docker-compose.yml", File.join(options.dir, "docker-compose.yml")
57
- template "gitignore", File.join(options.dir, ".gitignore")
57
+ template "Dockerfile", File.join(options.dir, "Dockerfile")
58
+ template ".dockerignore", File.join(options.dir, ".dockerignore")
59
+ template ".gitignore", File.join(options.dir, ".gitignore")
58
60
  template "README.md", File.join(options.dir, "README.md")
61
+ template "Gemfile", File.join(options.dir, "Gemfile")
59
62
  run tf_init
60
63
  say "Initializing git"
61
64
  inside options.dir do
@@ -67,22 +70,44 @@ module Polyn
67
70
  method_option :dir, default: Dir.getwd
68
71
  desc "tf_init", "Initializes Terraform for configuration"
69
72
  def tf_init
73
+ terraform_root = File.join(options.dir, "tf")
70
74
  say "Initializing Terraform"
71
- inside File.join(options.dir, "tf") do
72
- run "terraform init"
75
+ inside terraform_root do
76
+ # In a development environment we want developers to work with their own local
77
+ # .tfstate rather than one configured in a remote `backend` intended for
78
+ # production use.
79
+ # https://www.terraform.io/language/settings/backends/configuration
80
+ #
81
+ # Terraform assumes only one backend will be configured and there's no path
82
+ # to switch between local and remote. There's also no way to dynamically load
83
+ # modules. https://github.com/hashicorp/terraform/issues/1439
84
+ # Instead we'll copy a backend config to the terraform root if we're in a production
85
+ # environment
86
+ if polyn_env == "production"
87
+ add_remote_backend(terraform_root) { run "terraform init" }
88
+ else
89
+ run "terraform init"
90
+ end
73
91
  end
74
92
  end
75
93
 
76
94
  desc "up", "updates the JetStream streams and consumers, as well the Polyn event registry"
77
95
  def up
78
- if polyn_env == "development"
96
+ terraform_root = File.join(Dir.getwd, "tf")
97
+ # We only want to run nats in the docker container if
98
+ # the developer isn't already running nats themselves locally
99
+ if polyn_env == "development" && !nats_running?
79
100
  say "Starting NATS"
80
101
  run "docker compose up --detach"
81
102
  end
82
103
 
83
104
  say "Updating JetStream configuration"
84
105
  inside "tf" do
85
- run tf_apply
106
+ if polyn_env == "production"
107
+ add_remote_backend(terraform_root) { run tf_apply }
108
+ else
109
+ run tf_apply
110
+ end
86
111
  end
87
112
 
88
113
  say "Updating Polyn event registry"
@@ -99,14 +124,35 @@ module Polyn
99
124
  Polyn::Cli.configuration.nats_servers
100
125
  end
101
126
 
127
+ def nats_credentials
128
+ Polyn::Cli.configuration.nats_credentials
129
+ end
130
+
102
131
  def tf_apply
103
132
  if polyn_env == "development"
104
- %(terraform apply -var "jetstream_servers=#{nats_servers}" -auto-approve)
133
+ %(terraform apply -auto-approve -input=false -var "jetstream_servers=#{nats_servers}")
105
134
  else
106
- %(terraform apply -var "jetstream_servers=#{nats_servers}")
135
+ "terraform apply -auto-approve -input=false "\
136
+ "-var \"jetstream_servers=#{nats_servers}\" "\
137
+ "-var \"nats_credentials=#{nats_credentials}\" " \
138
+ "-var \"polyn_env=production\""
107
139
  end
108
140
  end
109
141
 
142
+ def nats_running?
143
+ # Uses lsof command to look up a process id. Will return `true` if it finds one
144
+ system("lsof -i TCP:4222 -t")
145
+ end
146
+
147
+ def add_remote_backend(tf_root)
148
+ copy_file File.join(tf_root, "remote_state_config/backend.tf"), "backend.tf"
149
+ yield
150
+ # We always want to remove the backend.tf file even if there's an error
151
+ # this way you don't get into a weird state when testing locally
152
+ ensure
153
+ remove_file File.join(tf_root, "backend.tf")
154
+ end
155
+
110
156
  register(Polyn::Cli::SchemaGenerator, "gen:schema", "gen:schema EVENT_TYPE",
111
157
  "Generates a new JSON Schema file for an event")
112
158
  register(Polyn::Cli::StreamGenerator, "gen:stream", "gen:stream NAME",
@@ -0,0 +1,6 @@
1
+ .git
2
+ tf/.terraform
3
+ tf/*.tfstate
4
+ tf/*.backup
5
+ # We don't ignore the terraform lockfile because it ensures that
6
+ # the provider dependency versions are consistent
@@ -0,0 +1,5 @@
1
+ tf/.terraform/**
2
+ *.tfstate
3
+ *.backup
4
+ # We don't ignore the terraform lockfile because it ensures that
5
+ # the provider dependency versions are consistent
@@ -0,0 +1,16 @@
1
+ FROM ruby:3.0.4-alpine3.15 as base
2
+ RUN apk add terraform
3
+ ADD Gemfile* ./
4
+ RUN gem install bundler
5
+ RUN bundle install
6
+
7
+ FROM base as app
8
+ WORKDIR /events
9
+ ADD events ./events
10
+ ADD tf ./tf
11
+
12
+ FROM app as dev
13
+ ENV POLYN_ENV='development'
14
+
15
+ FROM app as prod
16
+ ENV POLYN_ENV='production'
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "polyn-cli", "~> <%= Polyn::Cli::VERSION %>"
@@ -3,27 +3,35 @@
3
3
  This repository contains all of the events and terraform resources for the Polyn services
4
4
  environment.
5
5
 
6
- ## Development Setup
7
-
8
6
  1. Install [Ruby](https://github.com/asdf-vm/asdf-ruby)
9
- 2. Install Terraform. For M1 Macs, [download the AMD64 version](https://www.terraform.io/downloads)
7
+ 2. Install bundler `gem install bundler`
8
+ 3. Install dependencies `bundle install`
9
+ 4. Install Terraform. For M1 Macs, [download the AMD64 version](https://www.terraform.io/downloads)
10
10
  rather than using Homebrew to install Terraform.
11
- 3. Ensure Docker & Docker Compose is installed
12
- 4. [Install the Polyn CLI]()
13
- 5. Call `polyn up`. By default this will run in `development` mode, which will start the NATS
11
+ 5. Ensure Docker & Docker Compose is installed
12
+ 6. Call `bundle exec polyn tf_init` if this is the first time using terraform in the codebase.
13
+ 7. Call `bundle exec polyn up`. By default this will run in `development` mode, which will start the NATS
14
14
  server, configure it via Terraform, and update the Polyn Event Registry.
15
15
 
16
+ ### Running NATS locally
17
+
18
+ `bundle exec polyn up` will use run a Docker container for you if one is not already running. Alternatively, you can run `nats-server` yourself locally if you prefer.
19
+
20
+ ## Naming Conventions
21
+
22
+ See the Protocol Documentation for [Naming Conventions](https://github.com/SpiffInc/polyn-protocol/blob/main/NAMING_CONVENTIONS.md)
23
+
16
24
  ## Streams
17
25
 
18
- Each stream should have its own configuration file under `./tf` . Run `polyn gen:stream <stream_name>` to generate a new configuration file for a stream
26
+ Each stream should have its own configuration file under `./tf`. Run `bundle exec polyn gen:stream <stream_name>` to generate a new configuration file for a stream
19
27
 
20
28
  ## Consumers
21
29
 
22
- Run `polyn gen:consumer <stream_name> <destination_name> <event_type>` to generate new configuration for a consumer of a stream. It will be included in the same file as the stream configuration.
30
+ Run `bundle exec polyn gen:consumer <stream_name> <destination_name> <event_type>` to generate new configuration for a consumer of a stream. It will be included in the same file as the stream configuration.
23
31
 
24
32
  ## Event Schemas
25
33
 
26
- Run `polyn gen:schema <event_type>` to generate a new JSON Schema for an event
34
+ Run `bundle exec polyn gen:schema <event_type>` to generate a new JSON Schema for an event
27
35
 
28
36
  All the schemas for your events should live in the `./events` directory.
29
37
  The name of your schema file should be the same as your event, but with `.json` at the end.
@@ -32,6 +40,12 @@ Every schema should be a valid [JSON Schema](https://json-schema.org/) document.
32
40
  The Polyn CLI tool will combine your event schema with the [Cloud Events Schema](https://cloudevents.io/) when it adds it to the Polyn Event Registry.
33
41
  This means you only need to include the JSON Schema for the `data` portion of the Cloud Event and not the entire Cloud Event schema.
34
42
 
43
+ ### Subdirectories
44
+
45
+ If you'd like to organize your events by team ownership or some other convention, you can use subdirectories to do so. The full event type should still be part of the file name. You should also ensure there are not duplicate event types in different directories as only one schema can be defined per event type.
46
+
47
+ You can generate a schema in a subdirectory like this: `bundle exec polyn gen:schema some/nested/dir/widgets.created.v1`
48
+
35
49
  ## Schema Versioning
36
50
 
37
51
  ### New Event
@@ -49,3 +63,23 @@ Making a change to an event schema that is not backwards-compatible will require
49
63
  json file. The new file should have the same name as your old file, but with the version number increased. Your
50
64
  Producers will need to continue producing both events until you are sure there are no more consumers using the
51
65
  old event.
66
+
67
+ ## Terraform State
68
+
69
+ Terraform generates and maintains a [`terraform.tfstate`](https://www.terraform.io/language/state) file that is used to map terraform configuration to real production server instances. Polyn needs to interact with this file differently based on whether we are developing locally or in a production environment.
70
+
71
+ ### Local Development
72
+
73
+ For local development Polyn expects the `terraform.tfstate` file to exist in the local file system. However, it should not be checked in to version control. We don't want experiments and updates made on a local developer machines to end up as the "source of truth" for our production infrastucture.
74
+
75
+ ### Production
76
+
77
+ In production Terraform recommends keeping `terraform.tfstate` in a [remote storage location](https://www.terraform.io/language/state). The remote state file should be the "source of truth" for your infrastucture and shouldn't be getting accessed during development. Depending on the size of your organization and security policies, not all developers will have access to the remote storage source and you don't want that to prohibit them from adding events, streams, or consumers.
78
+
79
+ Polyn expects you to keep a `./remote_state_config/backend.tf` file that configures a Terraform [backend](https://www.terraform.io/language/settings/backends/configuration). This will only be used when `POLYN_ENV=production`.
80
+
81
+ ## Deployment
82
+
83
+ The default `Dockerfile` generated by [Install the Polyn CLI](https://github.com/SpiffInc/polyn-cli) can help you create an image with the latest changes and nessary environment to run polyn commands.
84
+
85
+ The `bin/apply_changes.sh` script can be used to execute the polyn commands you need to update your production NATS server. You'll need to pass in `env` variables for `NATS_SERVERS` and `NATS_CREDENTIALS`. Also any `env` variables needed to connect to your remote state storage.
@@ -0,0 +1,4 @@
1
+ provider "jetstream" {
2
+ servers = var.jetstream_servers
3
+ credentials = var.nats_credentials
4
+ }
@@ -0,0 +1,6 @@
1
+ terraform {
2
+ // Configure a [backend](https://www.terraform.io/language/settings/backends/configuration)
3
+ // to store your `terraform.tfstate` file in for production use
4
+ backend "remote" {
5
+ }
6
+ }
@@ -0,0 +1,15 @@
1
+ variable "jetstream_servers" {
2
+ type = string
3
+ description = "The JetStream servers to connect to"
4
+ }
5
+
6
+ variable "nats_credentials" {
7
+ type = string
8
+ description = "Path to file with NATS credentials"
9
+ }
10
+
11
+ variable "polyn_env" {
12
+ type = string
13
+ description = "The environment terraform is running in"
14
+ default = "development"
15
+ }
@@ -0,0 +1,8 @@
1
+ terraform {
2
+ required_providers {
3
+ jetstream = {
4
+ source = "nats-io/jetstream"
5
+ version = "~> 0.0.31"
6
+ }
7
+ }
8
+ }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: polyn-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jarod
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2022-08-10 00:00:00.000000000 Z
12
+ date: 2022-08-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: dotenv
@@ -89,6 +89,7 @@ files:
89
89
  - LICENSE.txt
90
90
  - README.md
91
91
  - Rakefile
92
+ - bin/apply_changes.sh
92
93
  - bin/console
93
94
  - bin/setup
94
95
  - exe/polyn
@@ -102,15 +103,21 @@ files:
102
103
  - lib/polyn/cli/stream_generator.rb
103
104
  - lib/polyn/cli/version.rb
104
105
  - lib/polyn/cloud-event-schema.json
106
+ - lib/polyn/templates/.dockerignore
107
+ - lib/polyn/templates/.gitignore
108
+ - lib/polyn/templates/Dockerfile
109
+ - lib/polyn/templates/Gemfile
105
110
  - lib/polyn/templates/README.md
106
111
  - lib/polyn/templates/docker-compose.yml
107
112
  - lib/polyn/templates/events/.gitkeep
108
113
  - lib/polyn/templates/events/widgets.created.v1.json
109
114
  - lib/polyn/templates/generators/schema.json
110
115
  - lib/polyn/templates/generators/stream.tf
111
- - lib/polyn/templates/gitignore
112
- - lib/polyn/templates/tf/event_registry.tf
113
- - lib/polyn/templates/tf/main.tf
116
+ - lib/polyn/templates/tf/kv_buckets.tf
117
+ - lib/polyn/templates/tf/provider.tf
118
+ - lib/polyn/templates/tf/remote_state_config/backend.tf
119
+ - lib/polyn/templates/tf/variables.tf
120
+ - lib/polyn/templates/tf/versions.tf
114
121
  - lib/polyn/templates/tf/widgets.tf
115
122
  - polyn-cli.gemspec
116
123
  homepage: https://github.com/Spiffinc/polyn-cli
@@ -1,3 +0,0 @@
1
- tf/.terraform/**
2
- *.tfstate
3
- .terraform.lock.hcl
@@ -1,17 +0,0 @@
1
- terraform {
2
- required_providers {
3
- jetstream = {
4
- source = "nats-io/jetstream"
5
- }
6
- }
7
-
8
- }
9
-
10
- variable "jetstream_servers" {
11
- type = string
12
- description = "The JetStream servers to connect to"
13
- }
14
-
15
- provider "jetstream" {
16
- servers = var.jetstream_servers
17
- }