[RUBY] How to make a Vagrant Plugin that you learned when you forked and published vagrant-mutagen

How to make a Vagrant Plugin that you learned when you forked and published vagrant-mutagen

Introduction

The Vagrant Plugin is an extension that allows you to add functionality to Vagrant.

Recently, mutagen is used for file synchronization when starting a VM, and the mutagen project is linked with starting / stopping the VM [vagrant-mutagen plugin](https://github.com/dasginganinja/ I was thinking of using vagrant-mutagen).

However, it didn't work properly on Windows, and I was using the modified version with PR, but it still starts the VM. I was in trouble because I had to elevate my administrator privileges twice each time.

I decided to fork and create a Vagrant Plugin at will, and I decided to summarize what I investigated at that time.

The source code of the forked plugin can be found at here, so please refer to it as an example.

Vagrant Plugin overview

For the outline of how to make it, I referred to the one described below.

https://www.vagrantup.com/docs/plugins/development-basics

How to write a Gemfile

my-vagrant-plugin is the name of the plugin you create.

source "https://rubygems.org"

group :development do
  gem "vagrant", git: "https://github.com/hashicorp/vagrant.git"
end

group :plugins do
  gem "my-vagrant-plugin", path: "."
end

It requires Vagrant for development, and in development mode the vagrant plugin command doesn't work and instead the gem specified in the plugins group is loaded.

I didn't usually have Ruby development in the Windows environment, so when developing the Vagrant plugin, I used the one installed on Windows, and each time I gem build the Plugin and uninstall / install it with vagrant. ..

  1. Make the Plugin a gem
  1. Install Plugin

(I thought I wouldn't make a big fix, but looking back, I think it was easier to get Vagrant running in development mode.)

Basic design of Plugin

Define the class that is the main body of Plugin, and specify the component class name such as Config, Provisioner in the class.

You need to require the Plugin class from the file that corresponds to the name listed in gemspec.

lib/<SOME_PLUGIN_NAME>/plugin.rb


class MyPlugin < Vagrant.plugin("2")
  name "My Plugin"

  command "run-my-plugin" do
    require_relative "command"
    Command
  end

  provisioner "my-provisioner" do
    require_relative "provisioner"
    Provisioner
  end
end

In the vagrant-mutagen-utilizer I created, it looks like this:

lib/vagrant_mutagen_utilizer/plugin.rb


# frozen_string_literal: true

require_relative 'action/update_config'
require_relative 'action/remove_config'
require_relative 'action/start_orchestration'
require_relative 'action/terminate_orchestration'
require_relative 'action/save_machine_identifier'

module VagrantPlugins
  module MutagenUtilizer
    # Plugin to utilize mutagen
    class MutagenUtilizerPlugin < Vagrant.plugin('2')
      name 'Mutagen Utilizer'
      description <<-DESC
        This plugin manages the ~/.ssh/config file for the host machine. An entry is
        created for the hostname attribute in the vm.config.
      DESC

      config(:mutagen_utilizer) do
        require_relative 'config'
        Config
      end

      action_hook(:mutagen_utilizer, :machine_action_up) do |hook|
        hook.append(Action::UpdateConfig)
        hook.append(Action::StartOrchestration)
      end
        : <snip>
    end
  end
end

vagrant-mutagen-utilizer.gemspec


lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'vagrant_mutagen_utilizer/version'

Gem::Specification.new do |spec|
  spec.name          = 'vagrant-mutagen-utilizer'
    : <snip>
end

lib/vagrant-mutagen-utilizer.rb


# frozen_string_literal: true

require 'vagrant_mutagen_utilizer/version'
require 'vagrant_mutagen_utilizer/plugin'

module VagrantPlugins
  # MutagenUtilizer
  module MutagenUtilizer
    def self.source_root
      @source_root ||= Pathname.new(File.expand_path('..', __dir__))
    end
  end
end

Forked vagrant-mutagen was originally like that, but instead of describing various processes in the Plugin body, class is described separately for each process, read using require_relative, and registered as a component. ..

How to set Config

Specify the class name on the Plugin body side. The "foo" specified here corresponds to config.foo. ~ In the Vagrantfile.

config "foo" do
  require_relative "config"
  Config
end

The Config component is defined as a subclass of Vagrant.plugin (2,: config).

class Config < Vagrant.plugin(2, :config)
  attr_accessor :widgets

  def initialize
    @widgets = UNSET_VALUE
  end

  def finalize!
    @widgets = 0 if @widgets == UNSET_VALUE
  end
end

UNSET_VALUE is a value that points to undefined in Vagrant. Some plugins provide undefined values so that you can define nil as an initial value or a correct value.

initialize is the constructor when the Config class is initialized, and finalize! Is called when all Configs have been read.

In the example, 0 is assigned if @widgets is not defined in the Config file.

The value set in Config can be accessed as machine.config.mutagen_utilizer.orchestrate.

Action Hooks

Set this when you want to perform specific processing when vagrant up, vagrant halt, etc. are executed.

Hook types can be found at https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks.

In the following, the UpdateConfig and StartOrchestration classes are set to be called when vagrant up is executed.

lib/vagrant_mutagen_utilizer/plugin.rb


# frozen_string_literal: true

require_relative 'action/update_config'
require_relative 'action/start_orchestration'
        : <snip>

module VagrantPlugins
  module MutagenUtilizer
    # Plugin to utilize mutagen
    class MutagenUtilizerPlugin < Vagrant.plugin('2')
        : <snip>
      action_hook(:mutagen_utilizer, :machine_action_up) do |hook|
        hook.append(Action::UpdateConfig)
        hook.append(Action::StartOrchestration)
      end
        : <snip>
    end
  end
end

The UpdateConfig and StartOrchestration classes are defined as follows:

lib/vagrant_mutagen_utilizer/action/update_config.rb


# frozen_string_literal: true

require_relative '../orchestrator'

module VagrantPlugins
  module MutagenUtilizer
    module Action
      # Update ssh config entry
      # If ssh config entry already exists, just entry appended
      class UpdateConfig
        def initialize(app, env)
          @app = app
          @machine = env[:machine]
          @config = env[:machine].config
          @console = env[:ui]
        end

        def call(env)
          return unless @config.orchestrate?

          o = Orchestrator.new(@machine, @console)
          o.update_ssh_config_entry
          @app.call(env)
        end
      end
    end
  end
end

lib/vagrant_mutagen_utilizer/action/start_orchestration.rb


# frozen_string_literal: true

require_relative '../orchestrator'

module VagrantPlugins
  module MutagenUtilizer
    module Action
      # Start mutagen project
      class StartOrchestration
        def initialize(app, env)
          @app = app
          @machine = env[:machine]
          @config = env[:machine].config
          @console = env[:ui]
        end

        def call(env)
          return unless @config.orchestrate?

          o = Orchestrator.new(@machine, @console)
          o.start_orchestration
          @app.call(env)
        end
      end
    end
  end
end

In both UpdateConfig and StartOrchestration, the constructor only registers the value received from the argument in the instance variable.

--@app: Used when the call method is executed. Details are unknown. --@machine: You can refer to the ID and Config related to the VM. --@config: You can refer to the settings --@ console: Console input / output is possible

The main functionality is written in the call method. Here we check if the Plugin is enabled in Config, and if so, we call the start_orchestration method of the Orchestrattor class.

The Orchestrattor class is:

# frozen_string_literal: true

module VagrantPlugins
  module MutagenUtilizer
    # Class for orchestrate with mutagen
    class Orchestrator
      def initialize(machine, console)
        @machine = machine
        @console = console
      end

      # Update ssh config entry
      # If ssh config entry already exists, just entry appended
      def update_ssh_config_entry
        hostname = @machine.config.vm.hostname

        logging(:info, 'Checking for SSH config entries')
        if ssh_config_entry_exist?
          logging(:info, "  updating SSH Config entry for: #{hostname}")
          remove_from_ssh_config
        else
          logging(:info, "  adding entry to SSH config for: #{hostname}")
        end
        append_to_ssh_config(ssh_config_entry)
      end

      def start_orchestration
        return if mutagen_project_started?

        logging(:info, 'Starting mutagen project orchestration (config: /mutagen.yml)')
        start_mutagen_project || logging(:error, 'Failed to start mutagen project (see error above)')
        # show project status to indicate if there are conflicts
        list_mutagen_project
      end

        : <snip>

      private

        : <snip>
    end
  end
end

Although details are omitted, update_ssh_config_entry called from Action Hooks describes the process of adding an entry to the SSH configuration file, and start_orchestration describes the process of starting a mutagen project.

In this way, you can write a plugin that does a specific thing when vagrant up is executed.

If you want to know the filters other than vagrant up, you can refer to https://www.vagrantup.com/docs/plugins/action-hooks#public-action-hooks.

Console input / output

It has already appeared several times, but it inputs and outputs text via Vagrant :: UI for Vagrant I / O. I can't use the standard Ruby put / get.

The Environment for each middleware, ʻenv, allows you to get the Vagrant :: UI with ʻenv [: ui].

There are log output levels of info, warn, error, success, and you can output logs at each level.

Publishing Plugin

The procedure is the same as exposing the gem to rubygems.

When using the rake command (Vagrant officially introduced as an easy way)

  1. bundle install
  2. rake build
  3. rake release

When using the gem command

  1. gem build
  2. gem push

Recommended Posts

How to make a Vagrant Plugin that you learned when you forked and published vagrant-mutagen
How to make a Jenkins plugin
How to make an app with a plugin mechanism [C # and Java]
How to make a Java container
How to make a JDBC driver
How to make a splash screen
How to make a Maven project
How to identify the path that is easy to make a mistake
How to make a Java array
When you receive a call, send an SMS to that number
How to test a private method in Java and partially mock that method
How to display error messages and success messages when registering as a user
JVM Performance Tuning: What is Tuning and How to Make a Good Plan
How to create a convenient method that utilizes generics and functional interfaces
How to make a Java calendar Summary
How to make a Discord bot (Java)
[Java] Eclipse shortcuts that make sense when you go back and forth between source code in a project
What to do when you want to delete a migration file that is "NO FILE"
How to make @Transactional work that does not work if you use it incorrectly
A story when I tried to make a video by linking Processing and Resolume
Find a value that is convenient to have a method and make it a ValueObject
How to get only articles and transactions that you commented or messaged [includes]
[Rails] How to write when making a subquery
How to create a class that inherits class information
How to make a lightweight JRE for distribution
I tried to make an app that allows you to post and chat by genre ~ Where I had a hard time ~
How to make Unity Native Plugin (Android version)
How to make a follow function in Rails
You don't have to write for twice when you make a right triangle in Java
[Ruby] 5 errors that tend to occur when scraping with Selenium and how to deal with them
How to quit Docker for Mac and build a Docker development environment with Ubuntu + Vagrant
How to make a factory with a model with polymorphic association
How to make JavaScript work on a specific page
How to think when you suddenly understand about generics
How to remove Ubuntu when dual booting Windows and Ubuntu
How to convert A to a and a to A using AND and OR in Java
How to make a cache without thinking too much
How to make a mod for Slay the Spire
A memo to check when you try to use Lombok
How to batch initialize arrays in Java that I didn't know when I was a beginner
[Rails Tutorial Chapter 2] What to do when you make a mistake in the column name
I tried to make an app that allows you to post and chat by genre ~ App overview ~
How to create an environment that uses Docker (WSL2) and Vagrant (VirtualBox) together on Windows