polyn-cli 0.1.0

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.
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyn
4
+ class Cli
5
+ ##
6
+ # Loads the JSON schmea into the event registry.
7
+ class SchemaLoader
8
+ include Thor::Actions
9
+
10
+ STORE_NAME = "POLYN_SCHEMAS"
11
+
12
+ ##
13
+ # Loads the events from the event repository into the Polyn event registry.
14
+ # @return [Bool]
15
+ def self.load(cli)
16
+ new(cli).load_events
17
+ end
18
+
19
+ def initialize(thor, **opts)
20
+ @thor = thor
21
+ @client = NATS.connect(Polyn::Cli.configuration.nats_servers).jetstream
22
+ @bucket = client.key_value(opts.fetch(:store_name, STORE_NAME))
23
+ @cloud_event_schema = Polyn::Cli::CloudEvent.to_h.freeze
24
+ @events_dir = opts.fetch(:events_dir, File.join(Dir.pwd, "events"))
25
+ @events = {}
26
+ end
27
+
28
+ def load_events
29
+ thor.say "Loading events into the Polyn event registry from '#{events_dir}'"
30
+ read_events
31
+
32
+ events.each do |name, event|
33
+ bucket.put(name, JSON.generate(event))
34
+ end
35
+
36
+ true
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :thor, :events, :client, :bucket, :cloud_event_schema, :events_dir
42
+
43
+ def read_events
44
+ Dir.glob(File.join(events_dir, "*.json")).each do |event_file|
45
+ thor.say "Loading 'event #{event_file}'"
46
+ data_schema = JSON.parse(File.read(event_file))
47
+ event_type = File.basename(event_file, ".json")
48
+ validate_schema!(event_type, data_schema)
49
+ Polyn::Cli::Naming.validate_event_type!(event_type)
50
+ schema = compose_cloud_event(data_schema)
51
+
52
+ events[event_type] = schema
53
+ end
54
+ end
55
+
56
+ def validate_schema!(event_type, schema)
57
+ JSONSchemer.schema(schema)
58
+ rescue StandardError => e
59
+ raise Polyn::Cli::ValidationError,
60
+ "Invalid JSON Schema document for event #{event_type}\n#{e.message}\n"\
61
+ "#{JSON.pretty_generate(schema)}"
62
+ end
63
+
64
+ def compose_cloud_event(event_schema)
65
+ cloud_event_schema.merge({
66
+ "definitions" => cloud_event_schema["definitions"].merge({
67
+ "datadef" => event_schema,
68
+ }),
69
+ })
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyn
4
+ class Cli
5
+ ##
6
+ # Generates a new Stream configuration file for terraform
7
+ class StreamGenerator < Thor::Group
8
+ include Thor::Actions
9
+
10
+ desc "Generates a new stream configuration with boilerplate"
11
+
12
+ argument :name, required: true
13
+ class_option :dir, default: Dir.getwd
14
+
15
+ source_root File.join(File.expand_path(__dir__), "../templates")
16
+
17
+ def check_name
18
+ Polyn::Cli::Naming.validate_stream_name!(name)
19
+ end
20
+
21
+ def file_name
22
+ @file_name ||= name.downcase
23
+ end
24
+
25
+ def stream_name
26
+ @stream_name ||= Polyn::Cli::Naming.format_stream_name(name)
27
+ end
28
+
29
+ def create
30
+ say "Creating new stream config #{stream_name}"
31
+ template "generators/stream.tf", File.join(options.dir, "tf/#{file_name}.tf")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polyn
4
+ class Cli
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/polyn/cli.rb ADDED
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "dotenv"
5
+ require "json_schemer"
6
+ require "polyn/cli/configuration"
7
+ require "polyn/cli/consumer_generator"
8
+ require "polyn/cli/naming"
9
+ require "polyn/cli/schema_generator"
10
+ require "polyn/cli/stream_generator"
11
+ require "polyn/cli/cloud_event"
12
+ require "polyn/cli/schema_loader"
13
+ require "polyn/cli/version"
14
+ require "json"
15
+ require "nats/client"
16
+
17
+ Dotenv.load
18
+
19
+ module Polyn
20
+ ##
21
+ # CLI for Polyn for configuring NATS server
22
+ class Cli
23
+ ##
24
+ # Proxy to Thor start
25
+ def self.start(args)
26
+ Commands.start(args)
27
+ end
28
+
29
+ class Error < StandardError; end
30
+ class ValidationError < Error; end
31
+
32
+ ##
33
+ # Configuration information for Polyn
34
+ def self.configuration
35
+ @configuration ||= Polyn::Cli::Configuration.new
36
+ end
37
+
38
+ ##
39
+ # Thor commands for the CLI. Subclassed so other classes can be in the CLI namespace
40
+ class Commands < Thor
41
+ include Thor::Actions
42
+
43
+ source_root File.join(File.expand_path(__dir__), "templates")
44
+
45
+ # https://github.com/rails/thor/wiki/Making-An-Executable
46
+ def self.exit_on_failure?
47
+ true
48
+ end
49
+
50
+ method_option :dir, default: Dir.getwd
51
+ desc "init", "initializes a Polyn event repository"
52
+ def init
53
+ say "Initializing Polyn event repository"
54
+ directory "tf", File.join(options.dir, "tf")
55
+ directory "events", File.join(options.dir, "events")
56
+ template "docker-compose.yml", File.join(options.dir, "docker-compose.yml")
57
+ template "gitignore", File.join(options.dir, ".gitignore")
58
+ template "README.md", File.join(options.dir, "README.md")
59
+ run tf_init
60
+ say "Initializing git"
61
+ inside options.dir do
62
+ run "git init"
63
+ end
64
+ say "Repository initialized"
65
+ end
66
+
67
+ desc "tf_init", "Initializes Terraform for configuration"
68
+ def tf_init
69
+ say "Initializing Terraform"
70
+ inside File.join(options.dir, "tf") do
71
+ run "terraform init"
72
+ end
73
+ end
74
+
75
+ desc "up", "updates the JetStream streams and consumers, as well the Polyn event registry"
76
+ def up
77
+ say "Updating JetStream configuration"
78
+ inside "tf" do
79
+ run tf_apply
80
+ end
81
+ say "Updating Polyn event registry"
82
+ Polyn::Cli::SchemaLoader.new(self).load_events
83
+ end
84
+
85
+ private
86
+
87
+ def polyn_env
88
+ Polyn::Cli.configuration.polyn_env
89
+ end
90
+
91
+ def nats_servers
92
+ Polyn::Cli.configuration.nats_servers
93
+ end
94
+
95
+ def tf_apply
96
+ if polyn_env == "development"
97
+ %(terraform apply -var "jetstream_servers=#{nats_servers}" -auto-approve)
98
+ else
99
+ %(terraform apply -var "jetstream_servers=#{nats_servers}")
100
+ end
101
+ end
102
+
103
+ register(Polyn::Cli::SchemaGenerator, "gen:schema", "gen:schema EVENT_TYPE",
104
+ "Generates a new JSON Schema file for an event")
105
+ register(Polyn::Cli::StreamGenerator, "gen:stream", "gen:stream NAME",
106
+ "Generates a new stream configuration with boilerplate")
107
+ register(Polyn::Cli::ConsumerGenerator, "gen:consumer",
108
+ "gen:consumer STREAM_NAME DESTINATION_NAME EVENT_TYPE",
109
+ "Generates a new NATS Consumer configuration with boilerplate")
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,187 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "description": "CloudEvents Specification JSON Schema, extended for Polyn",
4
+ "type": "object",
5
+ "properties": {
6
+ "id": {
7
+ "description": "Identifies the event.",
8
+ "$ref": "#/definitions/iddef",
9
+ "examples": [
10
+ "A234-1234-1234"
11
+ ]
12
+ },
13
+ "source": {
14
+ "description": "Identifies the context in which an event happened.",
15
+ "$ref": "#/definitions/sourcedef",
16
+ "examples" : [
17
+ "https://github.com/cloudevents",
18
+ "mailto:cncf-wg-serverless@lists.cncf.io",
19
+ "urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66",
20
+ "cloudevents/spec/pull/123",
21
+ "/sensors/tn-1234567/alerts",
22
+ "1-555-123-4567"
23
+ ]
24
+ },
25
+ "specversion": {
26
+ "description": "The version of the CloudEvents specification which the event uses.",
27
+ "$ref": "#/definitions/specversiondef",
28
+ "examples": [
29
+ "1.0"
30
+ ]
31
+ },
32
+ "type": {
33
+ "description": "Describes the type of event related to the originating occurrence.",
34
+ "$ref": "#/definitions/typedef",
35
+ "examples" : [
36
+ "com.github.pull_request.opened",
37
+ "com.example.object.deleted.v2"
38
+ ]
39
+ },
40
+ "datacontenttype": {
41
+ "description": "Content type of the data value. Must adhere to RFC 2046 format.",
42
+ "$ref": "#/definitions/datacontenttypedef",
43
+ "examples": [
44
+ "text/xml",
45
+ "application/json",
46
+ "image/png",
47
+ "multipart/form-data"
48
+ ]
49
+ },
50
+ "dataschema": {
51
+ "description": "Identifies the schema that data adheres to.",
52
+ "$ref": "#/definitions/dataschemadef"
53
+ },
54
+ "subject": {
55
+ "description": "Describes the subject of the event in the context of the event producer (identified by source).",
56
+ "$ref": "#/definitions/subjectdef",
57
+ "examples": [
58
+ "mynewfile.jpg"
59
+ ]
60
+ },
61
+ "time": {
62
+ "description": "Timestamp of when the occurrence happened. Must adhere to RFC 3339.",
63
+ "$ref": "#/definitions/timedef",
64
+ "examples": [
65
+ "2018-04-05T17:31:00Z"
66
+ ]
67
+ },
68
+ "data": {
69
+ "description": "The event payload.",
70
+ "$ref": "#/definitions/datadef",
71
+ "examples": [
72
+ "<much wow=\"xml\"/>"
73
+ ]
74
+ },
75
+ "data_base64": {
76
+ "description": "Base64 encoded event payload. Must adhere to RFC4648.",
77
+ "$ref": "#/definitions/data_base64def",
78
+ "examples": [
79
+ "Zm9vYg=="
80
+ ]
81
+ },
82
+ "polyndata": {
83
+ "$ref": "#/definitions/polyndatadef",
84
+ "description": "Information about the client that produced the event and additional metadata",
85
+ "examples": [
86
+ {
87
+ "clientlang": "elixir",
88
+ "clientlangversion": "1.13.2",
89
+ "clientversion": "0.1.0"
90
+ }
91
+ ]
92
+ },
93
+ "polyntrace": {
94
+ "$ref": "#/definitions/polyntracedef",
95
+ "description": "Previous events that led to this one",
96
+ "examples": [
97
+ [
98
+ {
99
+ "type": "<topic>",
100
+ "time": "2018-04-05T17:31:00Z",
101
+ "id": "<uuid>"
102
+ }
103
+ ]
104
+ ]
105
+ }
106
+ },
107
+ "required": ["id", "source", "specversion", "type"],
108
+ "definitions": {
109
+ "iddef": {
110
+ "type": "string",
111
+ "minLength": 1
112
+ },
113
+ "sourcedef": {
114
+ "type": "string",
115
+ "format": "uri-reference",
116
+ "minLength": 1
117
+ },
118
+ "specversiondef": {
119
+ "type": "string",
120
+ "minLength": 1
121
+ },
122
+ "typedef": {
123
+ "type": "string",
124
+ "minLength": 1
125
+ },
126
+ "datacontenttypedef": {
127
+ "type": ["string", "null"],
128
+ "minLength": 1
129
+ },
130
+ "dataschemadef": {
131
+ "type": ["string", "null"],
132
+ "format": "uri",
133
+ "minLength": 1
134
+ },
135
+ "subjectdef": {
136
+ "type": ["string", "null"],
137
+ "minLength": 1
138
+ },
139
+ "timedef": {
140
+ "type": ["string", "null"],
141
+ "format": "date-time",
142
+ "minLength": 1
143
+ },
144
+ "datadef": {
145
+ "type": ["object", "string", "number", "array", "boolean", "null"]
146
+ },
147
+ "data_base64def": {
148
+ "type": ["string", "null"],
149
+ "contentEncoding": "base64"
150
+ },
151
+ "polyndatadef": {
152
+ "type": "object",
153
+ "properties": {
154
+ "clientlang": {
155
+ "type": "string"
156
+ },
157
+ "clientlangversion": {
158
+ "type": "string"
159
+ },
160
+ "clientversion": {
161
+ "type": "string"
162
+ }
163
+ },
164
+ "required": ["clientlang", "clientlangversion", "clientversion"]
165
+ },
166
+ "polyntracedef": {
167
+ "type" : "array",
168
+ "items": {
169
+ "type": "object",
170
+ "properties": {
171
+ "type": {
172
+ "type": "string"
173
+ },
174
+ "time": {
175
+ "type": "string",
176
+ "format": "date-time"
177
+ },
178
+ "id" : {
179
+ "type": "string",
180
+ "format": "uuid"
181
+ }
182
+ },
183
+ "required": ["type", "time", "id"]
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,51 @@
1
+ # Polyn Events Repository
2
+
3
+ This repository contains all of the events and terraform resources for the Polyn services
4
+ environment.
5
+
6
+ ## Development Setup
7
+
8
+ 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)
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
14
+ server, configure it via Terraform, and update the Polyn Event Registry.
15
+
16
+ ## Streams
17
+
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
19
+
20
+ ## Consumers
21
+
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.
23
+
24
+ ## Event Schemas
25
+
26
+ Run `polyn gen:schema <event_type>` to generate a new JSON Schema for an event
27
+
28
+ All the schemas for your events should live in the `./events` directory.
29
+ The name of your schema file should be the same as your event, but with `.json` at the end.
30
+ So if you have an event called `widgets.created.v1` you would create a schema file called `widgets.created.v1.json` in the `./events` directory.
31
+ Every schema should be a valid [JSON Schema](https://json-schema.org/) document.
32
+ 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
+ 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
+
35
+ ## Schema Versioning
36
+
37
+ ### New Event
38
+
39
+ A new event schema file should be a lower-case, dot-separated, name with a `v1` suffix
40
+
41
+ ### Existing Event
42
+
43
+ Existing event schemas can be changed without updating the file name if the change is backwards-compatible.
44
+ Backwards-compatibile meaning that any services Producing or Consuming the event will not break or be invalid when the
45
+ Polyn Event Registry is updated with the change. There are many ways to make breaking change and so you should be
46
+ careful when you do this.
47
+
48
+ Making a change to an event schema that is not backwards-compatible will require you to create a brand new
49
+ json file. The new file should have the same name as your old file, but with the version number increased. Your
50
+ Producers will need to continue producing both events until you are sure there are no more consumers using the
51
+ old event.
@@ -0,0 +1,9 @@
1
+ version: "3"
2
+
3
+ services:
4
+ nats:
5
+ image: nats:2.8.4
6
+ ports:
7
+ - "4222:4222"
8
+ command:
9
+ - -js
File without changes
@@ -0,0 +1,14 @@
1
+ {
2
+ "type": "object",
3
+ "properties": {
4
+ "id": {
5
+ "type": "string",
6
+ "description": "The id of the widget"
7
+ },
8
+ "name": {
9
+ "type": "string",
10
+ "description": "The name of the widget"
11
+ }
12
+ },
13
+ "required": ["id", "name"]
14
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$id": "<%= schema_id %>",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "description": "This is why this event exists and what it does",
5
+ "type": "object",
6
+ "properties": {
7
+
8
+ }
9
+ }
@@ -0,0 +1,10 @@
1
+ // STREAM DEFINITION
2
+
3
+ resource "jetstream_stream" "<%= stream_name %>" {
4
+ name = "<%= stream_name %>"
5
+ subjects = []
6
+ storage = "file"
7
+ max_age = 60 * 60 * 24 * 365 // 1 year
8
+ }
9
+
10
+ // CONSUMERS
@@ -0,0 +1,3 @@
1
+ tf/.terraform/**
2
+ *.tfstate
3
+ .terraform.lock.hcl
@@ -0,0 +1,3 @@
1
+ resource "jetstream_kv_bucket" "POLYN_SCHEMAS" {
2
+ name = "POLYN_SCHEMAS"
3
+ }
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,18 @@
1
+ // STREAM DEFINITION
2
+
3
+ resource "jetstream_stream" "WIDGETS" {
4
+ name = "WIDGETS"
5
+ subjects = ["widgets.>"]
6
+ storage = "file"
7
+ max_age = 60 * 60 * 24 * 365 // 1 year
8
+ }
9
+
10
+ // CONSUMERS
11
+
12
+ resource "jetstream_consumer" "created_notifier_widgets_created_v1" {
13
+ stream_id = jetstream_stream.WIDGETS.id
14
+ durable_name = "created_notifier_widgets_created_v1"
15
+ deliver_all = true
16
+ filter_subject = "widgets.created.v1"
17
+ sample_freq = 100
18
+ }
data/polyn-cli.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/polyn/cli/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "polyn-cli"
7
+ spec.version = Polyn::Cli::VERSION
8
+ spec.authors = ["Jarod", "Brandyn Bennett"]
9
+ spec.email = ["jarod.reid@spiff.com", "brandyn.bennett@spiff.com"]
10
+
11
+ spec.summary = "CLI for the Polyn service framework"
12
+ spec.description = "CLI for the Polyn service framework"
13
+ spec.homepage = "https://github.com/Spiffinc/polyn-cli"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "dotenv", "~> 2.7.6"
29
+ spec.add_dependency "json_schemer", "~> 0.2"
30
+ spec.add_dependency "nats-pure", "~> 2.0.0"
31
+ spec.add_dependency "thor", "~> 1.2.0"
32
+ end