Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Schema validator #176

Merged
merged 10 commits into from
Nov 25, 2024
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,27 @@ services:
user: "1000:1000"
```

### dip validate

Validates your dip.yml configuration against the JSON schema. The schema validation helps ensure your configuration is correct and follows the expected format.

```sh
dip validate
```

The validator will check:

- Required properties are present
- Property types are correct
- Values match expected patterns
- No unknown properties are used

If validation fails, you'll get detailed error messages indicating what needs to be fixed.

You can skip validation by setting `DIP_SKIP_VALIDATION` environment variable.

Add `# yaml-language-server: $schema=https://raw.githubusercontent.com/bibendi/dip/refs/heads/master/schema.json` to the top of your dip.yml to get schema validation in VSCode. Read more about [YAML Language Server](https://github.com/redhat-developer/vscode-yaml?tab=readme-ov-file#associating-schemas).

## Changelog

https://github.com/bibendi/dip/releases
6 changes: 5 additions & 1 deletion dip.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@ Gem::Specification.new do |spec|

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.glob("lib/**/*") + Dir.glob("exe/*") + %w[LICENSE.txt README.md]
spec.files = Dir.glob("lib/**/*") + Dir.glob("exe/*") + %w[LICENSE.txt README.md schema.json]
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.required_ruby_version = ">= 2.7"

spec.add_dependency "thor", ">= 0.20", "< 2"
spec.add_dependency "json-schema", "~> 5"
# public_suffix >= 6.0 requires Ruby >= 3.0, so we need to specify an upper bound
# to maintain compatibility with Ruby 2.7
spec.add_dependency "public_suffix", ">= 2.0.2", "< 6.0"

spec.add_development_dependency "bundler", ">= 1.15"
spec.add_development_dependency "pry-byebug", "~> 3"
Expand Down
2 changes: 2 additions & 0 deletions dip.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# yaml-language-server: $schema=./schema.json

version: '7'

compose:
Expand Down
131 changes: 131 additions & 0 deletions examples/dip.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
version: '8.1.0'

environment:
RAILS_ENV: development
NODE_ENV: development
DATABASE_URL: postgres://user:password@db:5432/myapp_development
REDIS_URL: redis://redis:6379/0
PORT: ${PORT:-3000}
APP_PORT: ${PORT:-3000}

compose:
files:
- docker-compose.yml
- docker-compose.override.yml
project_name: myapp_project
command: docker compose

interaction:
rails:
description: Run Rails commands
service: web
command: bundle exec rails
default_args: server -p 3000 -b 0.0.0.0
environment:
RAILS_LOG_TO_STDOUT: "true"
compose:
method: run
compose_method: up
run_options:
- service-ports
- rm
profiles:
- web
- development
shell: true
entrypoint: /docker-entrypoint.sh
runner: docker_compose
subcommands:
console:
description: Start Rails console
command: console
routes:
description: Show Rails routes
command: routes
db:
description: Database related commands
subcommands:
migrate:
description: Run database migrations
command: db:migrate
seed:
description: Seed the database
command: db:seed

npm:
description: Run npm commands
service: frontend
command: npm
compose:
method: run
profiles:
- frontend

psql:
description: Connect to PostgreSQL database
service: db
command: psql -h db -U user myapp_development
compose:
method: run
environment:
PGPASSWORD: password

rspec:
description: Run RSpec tests
service: web
command: bundle exec rspec
environment:
RAILS_ENV: test
compose:
method: run
run_options:
- rm
profiles:
- test

shell:
description: Start a shell in the web container
service: web
command: /bin/bash
compose:
method: run
run_options:
- rm

k8s:
description: Run kubectl commands
command: kubectl
runner: kubectl
entrypoint: kubectl
shell: false

brakeman:
description: Check brakeman sast
command: docker run another-image ...

rake:
description: Run Rake tasks
service: web
command: bundle exec rake

provision:
- dip compose down --volumes
- dip compose build
- dip rails db:create
- dip rails db:migrate
- dip rails db:seed
- dip npm install
- dip validate

kubectl:
namespace: myapp-development

modules:
- production

infra:
redis:
git: https://github.com/mycompany/redis-config.git
ref: main
elasticsearch:
path: ./infra/elasticsearch
11 changes: 10 additions & 1 deletion lib/dip/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

module Dip
class CLI < Thor
TOP_LEVEL_COMMANDS = %w[help version ls compose up stop down run provision ssh infra console].freeze
TOP_LEVEL_COMMANDS = %w[help version ls compose up stop down run provision ssh infra console validate]

class << self
# Hackery. Take the run method away from Thor so that we can redefine it.
Expand Down Expand Up @@ -117,6 +117,15 @@ def provision
end
end

desc "validate", "Validate the dip.yml file against the schema"
def validate
Dip.config.validate
puts "dip.yml is valid"
rescue Dip::Error => e
warn "Validation failed: #{e.message}"
exit 1
end

require_relative "cli/ssh"
desc "ssh", "ssh-agent container commands"
subcommand :ssh, Dip::CLI::SSH
Expand Down
29 changes: 27 additions & 2 deletions lib/dip/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "yaml"
require "erb"
require "pathname"
require "json-schema"
bibendi marked this conversation as resolved.
Show resolved Hide resolved

require "dip/version"
require "dip/ext/hash"
Expand Down Expand Up @@ -112,6 +113,24 @@ def to_h
end
end

def validate
bibendi marked this conversation as resolved.
Show resolved Hide resolved
raise Dip::Error, "Config file path is not set" if file_path.nil?
raise Dip::Error, "Config file not found: #{file_path}" unless File.exist?(file_path)

schema_path = File.join(File.dirname(__FILE__), "../../schema.json")
bibendi marked this conversation as resolved.
Show resolved Hide resolved
raise Dip::Error, "Schema file not found: #{schema_path}" unless File.exist?(schema_path)

data = YAML.load_file(file_path)
schema = JSON.parse(File.read(schema_path))
JSON::Validator.validate!(schema, data)
rescue Psych::SyntaxError => e
raise Dip::Error, "Invalid YAML syntax in config file: #{e.message}"
rescue JSON::Schema::ValidationError => e
data_display = data ? data.to_yaml.gsub("\n", "\n ") : "nil"
error_message = "Schema validation failed: #{e.message}\nInput data:\n #{data_display}"
raise Dip::Error, error_message
end

private

attr_reader :work_dir
Expand All @@ -129,8 +148,8 @@ def config

unless Gem::Version.new(Dip::VERSION) >= Gem::Version.new(config.fetch(:version))
raise VersionMismatchError, "Your dip version is `#{Dip::VERSION}`, " \
"but config requires minimum version `#{config[:version]}`. " \
"Please upgrade your dip!"
"but config requires minimum version `#{config[:version]}`. " \
"Please upgrade your dip!"
end

base_config = {}
Expand All @@ -155,6 +174,12 @@ def config
base_config.deep_merge!(self.class.load_yaml(override_finder.file_path)) if override_finder.exist?

@config = CONFIG_DEFAULTS.merge(base_config)

unless ENV.key?("DIP_SKIP_VALIDATION")
validate
end

@config
end

def config_missing_error(config_key)
Expand Down
Loading
Loading