polyn-cli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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