From e77825e112ac4f32ed7cf72509954a9323526f1a Mon Sep 17 00:00:00 2001 From: nagaya Date: Wed, 13 Nov 2024 11:32:09 +0900 Subject: [PATCH 1/3] Add dev environments --- Dockerfile | 12 ++++++++++++ README.md | 11 +++++++---- Rakefile | 16 +++++++++++++++- docker-compose.yml | 44 ++++++++++++++++++++++++++++++++------------ spec/spec_helper.rb | 34 +++++++++++++++++++++++++--------- 5 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a1bd9a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM ruby:3.1.6 + +WORKDIR /usr/src/app + +RUN mkdir -p /usr/src/app/lib/gratan +COPY Gemfile gratan.gemspec /usr/src/app/ +COPY lib/gratan/version.rb /usr/src/app/lib/gratan/ +RUN bundle install + +COPY . . + +ENTRYPOINT ["gratan"] diff --git a/README.md b/README.md index 4819a55..bb3ffd8 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,13 @@ end ## Run tests ```sh -bundle install -docker-compose up -d -bundle exec rake -# MYSQL57=1 bundle exec rake +docker compose build +# for MySQL 5.6 and 5.7 +docker compose run --rm rake +# for MySQL 5.6 +docker compose run --rm rake spec5_6 +# for MySQL 5.7 +docker compose run --rm rake spec5_7 ``` ## Similar tools diff --git a/Rakefile b/Rakefile index 6adbdd9..1066d01 100644 --- a/Rakefile +++ b/Rakefile @@ -2,4 +2,18 @@ require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new('spec') -task :default => :spec +task :default => :spec_all + +suffixes = %w(5_6 5_7) +task :spec_all => suffixes.map { |s| "spec#{s}" } + +suffixes.each do |suffix| + overwrite_host = ENV['MYSQL_HOST'].to_s.empty? + task "spec#{suffix}" do + if overwrite_host + ENV['MYSQL_HOST'] = "mysql#{suffix}" + end + ENV['MYSQL5_7'] = (suffix == '5_7' ? 1 : 0).to_s + Rake::Task['spec'].execute + end +end diff --git a/docker-compose.yml b/docker-compose.yml index 26713e6..007b046 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,32 @@ -mysql56: - image: "mysql:5.6" - ports: - - "14406:3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" -mysql57: - image: "mysql:5.7" - ports: - - "14407:3306" - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" +services: + mysql5_6: + image: "mysql:5.6" + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + healthcheck: + test: ["CMD", "mysql", "-h", "localhost", "-u", "root", "--execute", "SHOW DATABASES;"] + start_period: 30s + timeout: 5s + interval: 5s + retries: 5 + mysql5_7: + image: "mysql:5.7" + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + healthcheck: + test: ["CMD", "mysql", "-h", "localhost", "-u", "root", "--execute", "SHOW DATABASES;"] + start_period: 30s + timeout: 5s + interval: 5s + retries: 5 + rake: + build: . + depends_on: + mysql5_6: + condition: service_healthy + mysql5_7: + condition: service_healthy + entrypoint: ["rake"] + volumes: + - .:/usr/src/app + working_dir: /usr/src/app diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e927f8b..459a6b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -13,18 +13,34 @@ end end -def mysql57? - ENV['MYSQL57'] == '1' +def env_empty?(str) + str.nil? || str.empty? || str == '0' end -MYSQL_PORT = mysql57? ? 14407 : 14406 +def mysql5_7? + !env_empty?(ENV['MYSQL5_7']) +end + +MYSQL_PORT = if ENV['MYSQL_PORT'].blank? + 3306 + else + ENV['MYSQL_PORT'].to_i + end + +MYSQL_HOST = if ENV['MYSQL_HOST'].blank? + '127.0.0.1' + else + ENV['MYSQL_HOST'] + end + +MYSQL_USER = 'root' def mysql client = nil retval = nil begin - client = Mysql2::Client.new(host: '127.0.0.1', username: 'root', port: MYSQL_PORT) + client = Mysql2::Client.new(host: MYSQL_HOST, username: MYSQL_USER, port: MYSQL_PORT) retval = yield(client) ensure client.close if client @@ -127,7 +143,7 @@ def show_grants end end - if mysql57? + if mysql5_7? grants.each do |grant| end end @@ -141,14 +157,14 @@ def client(user_options = {}) end options = { - host: '127.0.0.1', - username: 'root', + host: MYSQL_HOST, + username: MYSQL_USER, port: MYSQL_PORT, ignore_user: IGNORE_USER, logger: Logger.new('/dev/null'), } - if mysql57? + if mysql5_7? options.update( override_sql_mode: true, use_show_create_user: true, @@ -189,7 +205,7 @@ def apply(cli = client) class Array def normalize - if mysql57? + if mysql5_7? self.map do |i| i.sub(/ IDENTIFIED BY PASSWORD '[^']+'/, '') .sub(/ REQUIRE \w+\b/, '') From 3aa174b6868adf889af0346130778c818cfa6799 Mon Sep 17 00:00:00 2001 From: nagaya Date: Wed, 13 Nov 2024 11:31:35 +0900 Subject: [PATCH 2/3] Add GitHub Action --- .github/workflows/test.yml | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5d174fa --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,56 @@ +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check_test_execution_conditions: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: xt0rted/block-autosquash-commits-action@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + rspec: + runs-on: ubuntu-latest + strategy: + matrix: + ruby: + - 3.0.6 + - 3.1.4 + mysql: + - 5.6 + - 5.7 + needs: + - check_test_execution_conditions + services: + mysql: + image: mysql:${{ matrix.mysql }} + env: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + options: >- + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - "3306:3306" + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run RSpec for ${{ matrix.mysql }} + run: bundle exec rake spec`echo ${{ matrix.mysql }} | awk -F. '{print $1"_"$2}'` + env: + MYSQL_HOST: 127.0.0.1 From c9e814fd820090ee8c02f35ea6468bb49a09a3c8 Mon Sep 17 00:00:00 2001 From: nagaya Date: Thu, 14 Nov 2024 15:13:49 +0900 Subject: [PATCH 3/3] Add mysql version layer --- .github/workflows/test.yml | 4 +- README.md | 6 +- Rakefile | 16 +- lib/gratan.rb | 4 +- lib/gratan/client.rb | 268 +----------------- lib/gratan/mysql5.rb | 2 + lib/gratan/mysql5/client.rb | 267 +++++++++++++++++ lib/gratan/{ => mysql5}/driver.rb | 2 +- .../change/change_grants_2_spec.rb | 0 .../change/change_grants_3_spec.rb | 0 .../change/change_grants_4_spec.rb | 0 .../change/change_grants_expired_spec.rb | 0 .../change/change_grants_func_prcd_spec.rb | 0 .../change_grants_ignore_not_exist_spec.rb | 0 .../change/change_grants_multi_hosts_spec.rb | 0 .../change/change_grants_regexp_spec.rb | 0 .../{ => mysql5}/change/change_grants_spec.rb | 0 .../change/change_grants_target_spec.rb | 0 .../change_grants_with_ignore_object_spec.rb | 0 .../{ => mysql5}/create/create_user_2_spec.rb | 0 .../{ => mysql5}/create/create_user_3_spec.rb | 0 .../create/create_user_multi_hosts_spec.rb | 0 .../create/create_user_regexp_spec.rb | 0 spec/{ => mysql5}/create/create_user_spec.rb | 0 .../create/create_user_target_spec.rb | 0 .../create/create_user_with_func_prcd_spec.rb | 0 .../create_user_with_ignore_object_spec.rb | 0 .../create/create_user_with_template_spec.rb | 0 spec/{ => mysql5}/drop/drop_user_2_spec.rb | 0 spec/{ => mysql5}/drop/drop_user_spec.rb | 0 spec/{ => mysql5}/drop/expire_user_spec.rb | 0 spec/{ => mysql5}/export/export_chunk_spec.rb | 0 .../export/export_func_prcd_spec.rb | 0 spec/{ => mysql5}/export/export_spec.rb | 0 .../export/export_with_ignore_object_spec.rb | 0 spec/{ => mysql5}/misc/misc_spec.rb | 0 spec/{ => mysql5}/misc/require_spec.rb | 0 37 files changed, 287 insertions(+), 282 deletions(-) create mode 100644 lib/gratan/mysql5.rb create mode 100644 lib/gratan/mysql5/client.rb rename lib/gratan/{ => mysql5}/driver.rb (99%) rename spec/{ => mysql5}/change/change_grants_2_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_3_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_4_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_expired_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_func_prcd_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_ignore_not_exist_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_multi_hosts_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_regexp_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_target_spec.rb (100%) rename spec/{ => mysql5}/change/change_grants_with_ignore_object_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_2_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_3_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_multi_hosts_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_regexp_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_target_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_with_func_prcd_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_with_ignore_object_spec.rb (100%) rename spec/{ => mysql5}/create/create_user_with_template_spec.rb (100%) rename spec/{ => mysql5}/drop/drop_user_2_spec.rb (100%) rename spec/{ => mysql5}/drop/drop_user_spec.rb (100%) rename spec/{ => mysql5}/drop/expire_user_spec.rb (100%) rename spec/{ => mysql5}/export/export_chunk_spec.rb (100%) rename spec/{ => mysql5}/export/export_func_prcd_spec.rb (100%) rename spec/{ => mysql5}/export/export_spec.rb (100%) rename spec/{ => mysql5}/export/export_with_ignore_object_spec.rb (100%) rename spec/{ => mysql5}/misc/misc_spec.rb (100%) rename spec/{ => mysql5}/misc/require_spec.rb (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d174fa..cc3f374 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,6 +51,6 @@ jobs: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run RSpec for ${{ matrix.mysql }} - run: bundle exec rake spec`echo ${{ matrix.mysql }} | awk -F. '{print $1"_"$2}'` + run: bundle exec rake spec env: - MYSQL_HOST: 127.0.0.1 + MYSQL5_7: ${{ matrix.mysql == '5.7' && '1' || '0' }} diff --git a/README.md b/README.md index bb3ffd8..92d2bf1 100644 --- a/README.md +++ b/README.md @@ -141,12 +141,10 @@ end ```sh docker compose build -# for MySQL 5.6 and 5.7 -docker compose run --rm rake # for MySQL 5.6 -docker compose run --rm rake spec5_6 +docker compose run --rm -e MYSQL_HOST=mysql5_6 rake spec # for MySQL 5.7 -docker compose run --rm rake spec5_7 +docker compose run --rm -e MYSQL_HOST=mysql5_7 -e MYSQL5_7=1 rake spec ``` ## Similar tools diff --git a/Rakefile b/Rakefile index 1066d01..6adbdd9 100644 --- a/Rakefile +++ b/Rakefile @@ -2,18 +2,4 @@ require 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new('spec') -task :default => :spec_all - -suffixes = %w(5_6 5_7) -task :spec_all => suffixes.map { |s| "spec#{s}" } - -suffixes.each do |suffix| - overwrite_host = ENV['MYSQL_HOST'].to_s.empty? - task "spec#{suffix}" do - if overwrite_host - ENV['MYSQL_HOST'] = "mysql#{suffix}" - end - ENV['MYSQL5_7'] = (suffix == '5_7' ? 1 : 0).to_s - Rake::Task['spec'].execute - end -end +task :default => :spec diff --git a/lib/gratan.rb b/lib/gratan.rb index ceb2f52..407c743 100644 --- a/lib/gratan.rb +++ b/lib/gratan.rb @@ -11,7 +11,9 @@ module Gratan; end require 'gratan/logger' require 'gratan/template_helper' require 'gratan/client' -require 'gratan/driver' +require 'gratan/mysql5' +require 'gratan/mysql5/client' +require 'gratan/mysql5/driver' require 'gratan/dsl' require 'gratan/dsl/validator' require 'gratan/dsl/context' diff --git a/lib/gratan/client.rb b/lib/gratan/client.rb index a85be8d..29bb85c 100644 --- a/lib/gratan/client.rb +++ b/lib/gratan/client.rb @@ -1,268 +1,18 @@ -class Gratan::Client - include Gratan::Logger::Helper - - def initialize(options = {}) - @options = options - @options[:identifier] ||= Gratan::Identifier::Null.new - client = Mysql2::Client.new(options) - @driver = Gratan::Driver.new(client, options) - end - - def export(options = {}) - options = @options.merge(options) - exported = Gratan::Exporter.export(@driver, options) - - if options[:chunk_by_user] - exported = chunk_by_user(exported) - end - - if block_given? - exported.sort_by {|user_host, attrs| - user_host[0].empty? ? 'root' : user_host[0] - }.chunk {|user_host, attrs| - user_host[0].empty? ? 'root' : user_host[0] - }.each {|user, grants| - h = {} - grants.sort_by {|k, v| k }.each {|k, v| h[k] = v } - dsl = Gratan::DSL.convert(h, options) - yield(user, dsl) - } - else - Gratan::DSL.convert(exported, options) - end - end +require 'forwardable' - def chunk_by_user(exported) - chunked = {} - - exported.sort_by {|user_host, attrs| - user_host[0] - }.chunk {|user_host, attrs| - user_host[0] - }.each {|user, grants| - merged_attrs = {} - hosts = [] - - grants.each do |user_host, attrs| - hosts << user_host[1] - merged_attrs.deep_merge!(attrs) - end - - user_host = [user, hosts.sort] - chunked[user_host] = merged_attrs - } - - chunked - end +class Gratan::Client + extend Forwardable - def apply(file, options = {}) - options = @options.merge(options) + delegate [:export, :apply] => :@client - in_progress do - walk(file, options) - end + def initialize(options = {}) + @client = get_instance(options) end private - def walk(file, options) - expected = load_file(file, options) - actual = Gratan::Exporter.export(@driver, options.merge(:with_identifier => true)) - - expected.each do |user_host, expected_attrs| - next if user_host[0] =~ options[:ignore_user] - - if options[:target_user] - next unless user_host[0] =~ options[:target_user] - end - - actual_attrs = actual.delete(user_host) - - if actual_attrs - walk_user(*user_host, expected_attrs, actual_attrs) - else - create_user(*user_host, expected_attrs) - end - end - - actual.each do |user_host, attrs| - next if user_host[0] =~ options[:ignore_user] - - if options[:target_user] - next unless user_host[0] =~ options[:target_user] - end - - drop_user(*user_host) - end - end - - def create_user(user, host, attrs) - attrs[:options] ||= {} - - unless attrs[:options].has_key?(:identified) - identified = @options[:identifier].identify(user, host) - - if identified - attrs = attrs.dup - attrs[:options] = attrs[:options].dup - attrs[:options][:identified] = identified - end - end - - @driver.create_user(user, host, attrs) - update! - end - - def drop_user(user, host) - @driver.drop_user(user, host) - update! - end - - def walk_user(user, host, expected_attrs, actual_attrs) - walk_options(user, host, expected_attrs[:options], actual_attrs[:options]) - walk_objects(user, host, expected_attrs[:objects], actual_attrs[:objects]) - end - - def walk_options(user, host, expected_options, actual_options) - if expected_options.has_key?(:identified) - walk_identified(user, host, expected_options[:identified], actual_options[:identified]) - end - - walk_required(user, host, expected_options[:required], actual_options[:required]) - end - - def walk_identified(user, host, expected_identified, actual_identified) - if actual_identified == 'PASSWORD ' - unless @options[:ignore_password_secret] - log(:warn, "cannot change the password (`PASSWORD `)", :color => :yellow) - end - elsif expected_identified != actual_identified - @driver.identify(user, host, expected_identified) - update! - end - end - - def walk_required(user, host, expected_required, actual_required) - if expected_required != actual_required - @driver.set_require(user, host, expected_required) - update! - end - end - - def walk_objects(user, host, expected_objects, actual_objects) - expected_objects.each do |object_or_regexp, expected_options| - @driver.expand_object(object_or_regexp).each do |object| - expected_options ||= {} - actual_options = actual_objects.delete(object) - - if actual_options - walk_object(user, host, object, expected_options, actual_options) - else - @driver.grant(user, host, object, expected_options) - update! - end - end - end - - actual_objects.each do |object, options| - options ||= {} - @driver.revoke(user, host, object, options) - update! - end - end - - def walk_object(user, host, object, expected_options, actual_options) - walk_with_option(user, host, object, expected_options[:with], actual_options[:with]) - walk_privs(user, host, object, expected_options[:privs], actual_options[:privs]) - end - - def walk_with_option(user, host, object, expected_with_option, actual_with_option) - expected_with_option = (expected_with_option || '').upcase - actual_with_option = (actual_with_option || '').upcase - - if expected_with_option != actual_with_option - @driver.update_with_option(user, host, object, expected_with_option) - update! - end - end - - def walk_privs(user, host, object, expected_privs, actual_privs) - expected_privs = normalize_privs(expected_privs) - actual_privs = normalize_privs(actual_privs) - - revoke_privs = actual_privs - expected_privs - grant_privs = expected_privs - actual_privs - - unless revoke_privs.empty? - if revoke_privs.length == 1 and revoke_privs[0] == 'USAGE' and not grant_privs.empty? - # nothing to do - else - @driver.revoke(user, host, object, :privs => revoke_privs) - update! - end - end - - unless grant_privs.empty? - @driver.grant(user, host, object, :privs => grant_privs) - update! - end - end - - def normalize_privs(privs) - privs.map do |priv| - priv = priv.split('(', 2) - priv[0].upcase! - - if priv[1] - priv[1] = priv[1].split(',').map {|i| i.gsub(')', '').strip }.sort.join(', ') - priv[1] << ')' - end - - priv = priv.join('(') - - if priv == 'ALL' - priv = 'ALL PRIVILEGES' - end - - priv - end - end - - def load_file(file, options) - if file.kind_of?(String) - open(file) do |f| - Gratan::DSL.parse(f.read, file, options) - end - elsif file.respond_to?(:read) - Gratan::DSL.parse(file.read, file.path, options) - else - raise TypeError, "can't convert #{file} into File" - end - end - - def in_progress - updated = false - - begin - @driver.disable_log_bin_local - @driver.override_sql_mode - @driver.set_wait_timeout - @updated = false - yield - updated = @updated - @driver.flush_privileges if updated - ensure - @updated = nil - end - - if @options[:dry_run] - false - else - updated - end - end - - def update! - @updated = true + def get_instance(options) + client = Mysql2::Client.new(options) + Gratan::Mysql5::Client.new(client, options) end end diff --git a/lib/gratan/mysql5.rb b/lib/gratan/mysql5.rb new file mode 100644 index 0000000..64936ed --- /dev/null +++ b/lib/gratan/mysql5.rb @@ -0,0 +1,2 @@ +class Gratan::Mysql5 +end diff --git a/lib/gratan/mysql5/client.rb b/lib/gratan/mysql5/client.rb new file mode 100644 index 0000000..918b88c --- /dev/null +++ b/lib/gratan/mysql5/client.rb @@ -0,0 +1,267 @@ +class Gratan::Mysql5::Client + include Gratan::Logger::Helper + + def initialize(client, options = {}) + @options = options + @options[:identifier] ||= Gratan::Identifier::Null.new + @driver = Gratan::Mysql5::Driver.new(client, options) + end + + def export(options = {}) + options = @options.merge(options) + exported = Gratan::Exporter.export(@driver, options) + + if options[:chunk_by_user] + exported = chunk_by_user(exported) + end + + if block_given? + exported.sort_by {|user_host, attrs| + user_host[0].empty? ? 'root' : user_host[0] + }.chunk {|user_host, attrs| + user_host[0].empty? ? 'root' : user_host[0] + }.each {|user, grants| + h = {} + grants.sort_by {|k, v| k }.each {|k, v| h[k] = v } + dsl = Gratan::DSL.convert(h, options) + yield(user, dsl) + } + else + Gratan::DSL.convert(exported, options) + end + end + + def chunk_by_user(exported) + chunked = {} + + exported.sort_by {|user_host, attrs| + user_host[0] + }.chunk {|user_host, attrs| + user_host[0] + }.each {|user, grants| + merged_attrs = {} + hosts = [] + + grants.each do |user_host, attrs| + hosts << user_host[1] + merged_attrs.deep_merge!(attrs) + end + + user_host = [user, hosts.sort] + chunked[user_host] = merged_attrs + } + + chunked + end + + def apply(file, options = {}) + options = @options.merge(options) + + in_progress do + walk(file, options) + end + end + + private + + def walk(file, options) + expected = load_file(file, options) + actual = Gratan::Exporter.export(@driver, options.merge(:with_identifier => true)) + + expected.each do |user_host, expected_attrs| + next if user_host[0] =~ options[:ignore_user] + + if options[:target_user] + next unless user_host[0] =~ options[:target_user] + end + + actual_attrs = actual.delete(user_host) + + if actual_attrs + walk_user(*user_host, expected_attrs, actual_attrs) + else + create_user(*user_host, expected_attrs) + end + end + + actual.each do |user_host, attrs| + next if user_host[0] =~ options[:ignore_user] + + if options[:target_user] + next unless user_host[0] =~ options[:target_user] + end + + drop_user(*user_host) + end + end + + def create_user(user, host, attrs) + attrs[:options] ||= {} + + unless attrs[:options].has_key?(:identified) + identified = @options[:identifier].identify(user, host) + + if identified + attrs = attrs.dup + attrs[:options] = attrs[:options].dup + attrs[:options][:identified] = identified + end + end + + @driver.create_user(user, host, attrs) + update! + end + + def drop_user(user, host) + @driver.drop_user(user, host) + update! + end + + def walk_user(user, host, expected_attrs, actual_attrs) + walk_options(user, host, expected_attrs[:options], actual_attrs[:options]) + walk_objects(user, host, expected_attrs[:objects], actual_attrs[:objects]) + end + + def walk_options(user, host, expected_options, actual_options) + if expected_options.has_key?(:identified) + walk_identified(user, host, expected_options[:identified], actual_options[:identified]) + end + + walk_required(user, host, expected_options[:required], actual_options[:required]) + end + + def walk_identified(user, host, expected_identified, actual_identified) + if actual_identified == 'PASSWORD ' + unless @options[:ignore_password_secret] + log(:warn, "cannot change the password (`PASSWORD `)", :color => :yellow) + end + elsif expected_identified != actual_identified + @driver.identify(user, host, expected_identified) + update! + end + end + + def walk_required(user, host, expected_required, actual_required) + if expected_required != actual_required + @driver.set_require(user, host, expected_required) + update! + end + end + + def walk_objects(user, host, expected_objects, actual_objects) + expected_objects.each do |object_or_regexp, expected_options| + @driver.expand_object(object_or_regexp).each do |object| + expected_options ||= {} + actual_options = actual_objects.delete(object) + + if actual_options + walk_object(user, host, object, expected_options, actual_options) + else + @driver.grant(user, host, object, expected_options) + update! + end + end + end + + actual_objects.each do |object, options| + options ||= {} + @driver.revoke(user, host, object, options) + update! + end + end + + def walk_object(user, host, object, expected_options, actual_options) + walk_with_option(user, host, object, expected_options[:with], actual_options[:with]) + walk_privs(user, host, object, expected_options[:privs], actual_options[:privs]) + end + + def walk_with_option(user, host, object, expected_with_option, actual_with_option) + expected_with_option = (expected_with_option || '').upcase + actual_with_option = (actual_with_option || '').upcase + + if expected_with_option != actual_with_option + @driver.update_with_option(user, host, object, expected_with_option) + update! + end + end + + def walk_privs(user, host, object, expected_privs, actual_privs) + expected_privs = normalize_privs(expected_privs) + actual_privs = normalize_privs(actual_privs) + + revoke_privs = actual_privs - expected_privs + grant_privs = expected_privs - actual_privs + + unless revoke_privs.empty? + if revoke_privs.length == 1 and revoke_privs[0] == 'USAGE' and not grant_privs.empty? + # nothing to do + else + @driver.revoke(user, host, object, :privs => revoke_privs) + update! + end + end + + unless grant_privs.empty? + @driver.grant(user, host, object, :privs => grant_privs) + update! + end + end + + def normalize_privs(privs) + privs.map do |priv| + priv = priv.split('(', 2) + priv[0].upcase! + + if priv[1] + priv[1] = priv[1].split(',').map {|i| i.gsub(')', '').strip }.sort.join(', ') + priv[1] << ')' + end + + priv = priv.join('(') + + if priv == 'ALL' + priv = 'ALL PRIVILEGES' + end + + priv + end + end + + def load_file(file, options) + if file.kind_of?(String) + open(file) do |f| + Gratan::DSL.parse(f.read, file, options) + end + elsif file.respond_to?(:read) + Gratan::DSL.parse(file.read, file.path, options) + else + raise TypeError, "can't convert #{file} into File" + end + end + + def in_progress + updated = false + + begin + @driver.disable_log_bin_local + @driver.override_sql_mode + @driver.set_wait_timeout + @updated = false + yield + updated = @updated + @driver.flush_privileges if updated + ensure + @updated = nil + end + + if @options[:dry_run] + false + else + updated + end + end + + def update! + @updated = true + end +end diff --git a/lib/gratan/driver.rb b/lib/gratan/mysql5/driver.rb similarity index 99% rename from lib/gratan/driver.rb rename to lib/gratan/mysql5/driver.rb index 9e24154..909e739 100644 --- a/lib/gratan/driver.rb +++ b/lib/gratan/mysql5/driver.rb @@ -1,4 +1,4 @@ -class Gratan::Driver +class Gratan::Mysql5::Driver include Gratan::Logger::Helper ER_NO_SUCH_TABLE = 1146 diff --git a/spec/change/change_grants_2_spec.rb b/spec/mysql5/change/change_grants_2_spec.rb similarity index 100% rename from spec/change/change_grants_2_spec.rb rename to spec/mysql5/change/change_grants_2_spec.rb diff --git a/spec/change/change_grants_3_spec.rb b/spec/mysql5/change/change_grants_3_spec.rb similarity index 100% rename from spec/change/change_grants_3_spec.rb rename to spec/mysql5/change/change_grants_3_spec.rb diff --git a/spec/change/change_grants_4_spec.rb b/spec/mysql5/change/change_grants_4_spec.rb similarity index 100% rename from spec/change/change_grants_4_spec.rb rename to spec/mysql5/change/change_grants_4_spec.rb diff --git a/spec/change/change_grants_expired_spec.rb b/spec/mysql5/change/change_grants_expired_spec.rb similarity index 100% rename from spec/change/change_grants_expired_spec.rb rename to spec/mysql5/change/change_grants_expired_spec.rb diff --git a/spec/change/change_grants_func_prcd_spec.rb b/spec/mysql5/change/change_grants_func_prcd_spec.rb similarity index 100% rename from spec/change/change_grants_func_prcd_spec.rb rename to spec/mysql5/change/change_grants_func_prcd_spec.rb diff --git a/spec/change/change_grants_ignore_not_exist_spec.rb b/spec/mysql5/change/change_grants_ignore_not_exist_spec.rb similarity index 100% rename from spec/change/change_grants_ignore_not_exist_spec.rb rename to spec/mysql5/change/change_grants_ignore_not_exist_spec.rb diff --git a/spec/change/change_grants_multi_hosts_spec.rb b/spec/mysql5/change/change_grants_multi_hosts_spec.rb similarity index 100% rename from spec/change/change_grants_multi_hosts_spec.rb rename to spec/mysql5/change/change_grants_multi_hosts_spec.rb diff --git a/spec/change/change_grants_regexp_spec.rb b/spec/mysql5/change/change_grants_regexp_spec.rb similarity index 100% rename from spec/change/change_grants_regexp_spec.rb rename to spec/mysql5/change/change_grants_regexp_spec.rb diff --git a/spec/change/change_grants_spec.rb b/spec/mysql5/change/change_grants_spec.rb similarity index 100% rename from spec/change/change_grants_spec.rb rename to spec/mysql5/change/change_grants_spec.rb diff --git a/spec/change/change_grants_target_spec.rb b/spec/mysql5/change/change_grants_target_spec.rb similarity index 100% rename from spec/change/change_grants_target_spec.rb rename to spec/mysql5/change/change_grants_target_spec.rb diff --git a/spec/change/change_grants_with_ignore_object_spec.rb b/spec/mysql5/change/change_grants_with_ignore_object_spec.rb similarity index 100% rename from spec/change/change_grants_with_ignore_object_spec.rb rename to spec/mysql5/change/change_grants_with_ignore_object_spec.rb diff --git a/spec/create/create_user_2_spec.rb b/spec/mysql5/create/create_user_2_spec.rb similarity index 100% rename from spec/create/create_user_2_spec.rb rename to spec/mysql5/create/create_user_2_spec.rb diff --git a/spec/create/create_user_3_spec.rb b/spec/mysql5/create/create_user_3_spec.rb similarity index 100% rename from spec/create/create_user_3_spec.rb rename to spec/mysql5/create/create_user_3_spec.rb diff --git a/spec/create/create_user_multi_hosts_spec.rb b/spec/mysql5/create/create_user_multi_hosts_spec.rb similarity index 100% rename from spec/create/create_user_multi_hosts_spec.rb rename to spec/mysql5/create/create_user_multi_hosts_spec.rb diff --git a/spec/create/create_user_regexp_spec.rb b/spec/mysql5/create/create_user_regexp_spec.rb similarity index 100% rename from spec/create/create_user_regexp_spec.rb rename to spec/mysql5/create/create_user_regexp_spec.rb diff --git a/spec/create/create_user_spec.rb b/spec/mysql5/create/create_user_spec.rb similarity index 100% rename from spec/create/create_user_spec.rb rename to spec/mysql5/create/create_user_spec.rb diff --git a/spec/create/create_user_target_spec.rb b/spec/mysql5/create/create_user_target_spec.rb similarity index 100% rename from spec/create/create_user_target_spec.rb rename to spec/mysql5/create/create_user_target_spec.rb diff --git a/spec/create/create_user_with_func_prcd_spec.rb b/spec/mysql5/create/create_user_with_func_prcd_spec.rb similarity index 100% rename from spec/create/create_user_with_func_prcd_spec.rb rename to spec/mysql5/create/create_user_with_func_prcd_spec.rb diff --git a/spec/create/create_user_with_ignore_object_spec.rb b/spec/mysql5/create/create_user_with_ignore_object_spec.rb similarity index 100% rename from spec/create/create_user_with_ignore_object_spec.rb rename to spec/mysql5/create/create_user_with_ignore_object_spec.rb diff --git a/spec/create/create_user_with_template_spec.rb b/spec/mysql5/create/create_user_with_template_spec.rb similarity index 100% rename from spec/create/create_user_with_template_spec.rb rename to spec/mysql5/create/create_user_with_template_spec.rb diff --git a/spec/drop/drop_user_2_spec.rb b/spec/mysql5/drop/drop_user_2_spec.rb similarity index 100% rename from spec/drop/drop_user_2_spec.rb rename to spec/mysql5/drop/drop_user_2_spec.rb diff --git a/spec/drop/drop_user_spec.rb b/spec/mysql5/drop/drop_user_spec.rb similarity index 100% rename from spec/drop/drop_user_spec.rb rename to spec/mysql5/drop/drop_user_spec.rb diff --git a/spec/drop/expire_user_spec.rb b/spec/mysql5/drop/expire_user_spec.rb similarity index 100% rename from spec/drop/expire_user_spec.rb rename to spec/mysql5/drop/expire_user_spec.rb diff --git a/spec/export/export_chunk_spec.rb b/spec/mysql5/export/export_chunk_spec.rb similarity index 100% rename from spec/export/export_chunk_spec.rb rename to spec/mysql5/export/export_chunk_spec.rb diff --git a/spec/export/export_func_prcd_spec.rb b/spec/mysql5/export/export_func_prcd_spec.rb similarity index 100% rename from spec/export/export_func_prcd_spec.rb rename to spec/mysql5/export/export_func_prcd_spec.rb diff --git a/spec/export/export_spec.rb b/spec/mysql5/export/export_spec.rb similarity index 100% rename from spec/export/export_spec.rb rename to spec/mysql5/export/export_spec.rb diff --git a/spec/export/export_with_ignore_object_spec.rb b/spec/mysql5/export/export_with_ignore_object_spec.rb similarity index 100% rename from spec/export/export_with_ignore_object_spec.rb rename to spec/mysql5/export/export_with_ignore_object_spec.rb diff --git a/spec/misc/misc_spec.rb b/spec/mysql5/misc/misc_spec.rb similarity index 100% rename from spec/misc/misc_spec.rb rename to spec/mysql5/misc/misc_spec.rb diff --git a/spec/misc/require_spec.rb b/spec/mysql5/misc/require_spec.rb similarity index 100% rename from spec/misc/require_spec.rb rename to spec/mysql5/misc/require_spec.rb