# Copyright:: Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require_relative "../resource" require "chef-utils/dist" unless defined?(ChefUtils::Dist) class Chef class Resource class HabitatService < Chef::Resource unified_mode true provides :habitat_service description "Use the **habitat_service** resource to manage Chef Habitat services. This requires that `core/hab-sup` be running as a service. See the `habitat_sup` resource documentation for more information. Note: Applications may run as a specific user. Often with Habitat, the default is `hab`, or `root`. If the application requires another user, then it should be created with Chef's `user` resource." introduced "17.3" examples <<~DOC **Install and load nginx** ```ruby habitat_package 'core/nginx' habitat_service 'core/nginx' habitat_service 'core/nginx unload' do service_name 'core/nginx' action :unload end ``` **Pass the `strategy` and `topology` options to hab service commands** ```ruby habitat_service 'core/redis' do strategy 'rolling' topology 'standalone' end ``` **Using update_condition** ```ruby habitat_service 'core/redis' do strategy 'rolling' update_condition 'track-channel' topology 'standalone' end ``` **If the service has it's own user specified that is not the `hab` user, don't create the `hab` user on install, and instead create the application user with Chef's `user` resource** ```ruby habitat_install 'install habitat' do create_user false end user 'acme-apps' do system true end habitat_service 'acme/apps' ``` DOC property :service_name, String, name_property: true, description: "The name of the service, must be in the form of `origin/name`" property :loaded, [true, false], default: false, skip_docs: true, description: "state property indicating whether the service is loaded in the supervisor" property :running, [true, false], default: false, skip_docs: true, description: "state property indicating whether the service is running in the supervisor" # hab svc options which get included based on the action of the resource property :strategy, [Symbol, String], equal_to: [:none, "none", :'at-once', "at-once", :rolling, "rolling"], default: :none, coerce: proc { |s| s.is_a?(String) ? s.to_sym : s }, description: "Passes `--strategy` with the specified update strategy to the hab command. Defaults to `:none`. Other options are `:'at-once'` and `:rolling`" property :topology, [Symbol, String], equal_to: [:standalone, "standalone", :leader, "leader"], default: :standalone, coerce: proc { |s| s.is_a?(String) ? s.to_sym : s }, description: "Passes `--topology` with the specified service topology to the hab command" property :bldr_url, String, default: "https://bldr.habitat.sh/", description: "Passes `--url` with the specified Habitat Builder URL to the hab command. Depending on the type of Habitat Builder you are connecting to, this URL will look different, here are the **3** current types: - Public Habitat Builder (default) - `https://bldr.habitat.sh` - On-Prem Habitat Builder installed using the [Source Install Method](https://github.com/habitat-sh/on-prem-builder) - `https://your.bldr.url` - On-Prem Habitat Builder installed using the [Automate Installer](https://automate.chef.io/docs/on-prem-builder/) - `https://your.bldr.url/bldr/v1`" property :channel, [Symbol, String], default: :stable, coerce: proc { |s| s.is_a?(String) ? s.to_sym : s }, description: "Passes `--channel` with the specified channel to the hab command" property :bind, [String, Array], coerce: proc { |b| b.is_a?(String) ? [b] : b }, default: [], description: "Passes `--bind` with the specified services to bind to the hab command. If an array of multiple service binds are specified then a `--bind` flag is added for each." property :binding_mode, [Symbol, String], equal_to: [:strict, "strict", :relaxed, "relaxed"], default: :strict, coerce: proc { |s| s.is_a?(String) ? s.to_sym : s }, description: "Passes `--binding-mode` with the specified binding mode. Defaults to `:strict`. Options are `:strict` or `:relaxed`" property :service_group, String, default: "default", description: " Passes `--group` with the specified service group to the hab command" property :shutdown_timeout, Integer, default: 8, description: "The timeout in seconds allowed during shutdown." property :health_check_interval, Integer, default: 30, description: "The interval (seconds) on which to run health checks." property :remote_sup, String, default: "127.0.0.1:9632", desired_state: false, description: "Address to a remote Supervisor's Control Gateway" # Http port needed for querying/comparing current config value property :remote_sup_http, String, default: "127.0.0.1:9631", desired_state: false, description: "IP address and port used to communicate with the remote supervisor. If this value is invalid, the resource will update the supervisor configuration each time #{ChefUtils::Dist::Server::PRODUCT} runs." property :gateway_auth_token, String, desired_state: false, description: "Auth token for accessing the remote supervisor's http port." property :update_condition, [Symbol, String], equal_to: [:latest, "latest", :'track-channel', "track-channel"], default: :latest, coerce: proc { |s| s.is_a?(String) ? s.to_sym : s }, description: "Passes `--update-condition` dictating when this service should updated. Defaults to `latest`. Options are `latest` or `track-channel` **_Note: This requires a minimum habitat version of 1.5.71_** - `latest`: Runs the latest package that can be found in the configured channel and local packages. - `track-channel`: Always run the package at the head of a given channel. This enables service rollback, where demoting a package from a channel will cause the package to rollback to an older version of the package. A ramification of enabling this condition is that packages that are newer than the package at the head of the channel are also uninstalled during a service rollback." load_current_value do service_details = get_service_details(service_name) running service_up?(service_details) loaded service_loaded?(service_details) if loaded service_name get_spec_identifier(service_details) strategy get_update_strategy(service_details) update_condition get_update_condition(service_details) topology get_topology(service_details) bldr_url get_builder_url(service_details) channel get_channel(service_details) bind get_binds(service_details) binding_mode get_binding_mode(service_details) service_group get_service_group(service_details) shutdown_timeout get_shutdown_timeout(service_details) health_check_interval get_health_check_interval(service_details) end Chef::Log.debug("service #{service_name} service name: #{service_name}") Chef::Log.debug("service #{service_name} running state: #{running}") Chef::Log.debug("service #{service_name} loaded state: #{loaded}") Chef::Log.debug("service #{service_name} strategy: #{strategy}") Chef::Log.debug("service #{service_name} update condition: #{update_condition}") Chef::Log.debug("service #{service_name} topology: #{topology}") Chef::Log.debug("service #{service_name} builder url: #{bldr_url}") Chef::Log.debug("service #{service_name} channel: #{channel}") Chef::Log.debug("service #{service_name} binds: #{bind}") Chef::Log.debug("service #{service_name} binding mode: #{binding_mode}") Chef::Log.debug("service #{service_name} service group: #{service_group}") Chef::Log.debug("service #{service_name} shutdown timeout: #{shutdown_timeout}") Chef::Log.debug("service #{service_name} health check interval: #{health_check_interval}") end # This method is defined here otherwise it isn't usable in the # `load_current_value` method. # # It performs a check with TCPSocket to ensure that the HTTP API is # available first. If it cannot connect, it assumes that the service # is not running. It then attempts to reach the `/services` path of # the API to get a list of services. If this fails for some reason, # then it assumes the service is not running. # # Finally, it walks the services returned by the API to look for the # service we're configuring. If it is "Up", then we know the service # is running and fully operational according to Habitat. This is # wrapped in a begin/rescue block because if the service isn't # present and `sup_for_service_name` will be nil and we will get a # NoMethodError. # def get_service_details(svc_name) http_uri = "http://#{remote_sup_http}" begin TCPSocket.new(URI(http_uri).host, URI(http_uri).port).close rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH Chef::Log.debug("Could not connect to #{http_uri} to retrieve status for #{service_name}") return false end begin headers = {} headers["Authorization"] = "Bearer #{gateway_auth_token}" if property_is_set?(:gateway_auth_token) svcs = Chef::HTTP::SimpleJSON.new(http_uri).get("/services", headers) rescue Chef::Log.debug("Could not connect to #{http_uri}/services to retrieve status for #{service_name}") return false end origin, name, _version, _release = svc_name.split("/") svcs.find do |s| s["pkg"]["origin"] == origin && s["pkg"]["name"] == name end end def service_up?(service_details) service_details["process"]["state"] == "up" rescue Chef::Log.debug("#{service_name} not found on the Habitat supervisor") false end def service_loaded?(service_details) if service_details true else false end end def get_spec_identifier(service_details) service_details["spec_ident"]["spec_identifier"] rescue Chef::Log.debug("#{service_name} not found on the Habitat supervisor") nil end def get_update_strategy(service_details) service_details["update_strategy"].to_sym rescue Chef::Log.debug("Update Strategy for #{service_name} not found on Supervisor API") "none" end def get_update_condition(service_details) service_details["update_condition"].to_sym rescue Chef::Log.debug("Update condition #{service_name} not found on Supervisor API") "latest" end def get_topology(service_details) service_details["topology"].to_sym rescue Chef::Log.debug("Topology for #{service_name} not found on Supervisor API") "standalone" end def get_builder_url(service_details) service_details["bldr_url"] rescue Chef::Log.debug("Habitat Builder URL for #{service_name} not found on Supervisor API") "https://bldr.habitat.sh" end def get_channel(service_details) service_details["channel"].to_sym rescue Chef::Log.debug("Channel for #{service_name} not found on Supervisor API") "stable" end def get_binds(service_details) service_details["binds"] rescue Chef::Log.debug("Update Strategy for #{service_name} not found on Supervisor API") [] end def get_binding_mode(service_details) service_details["binding_mode"].to_sym rescue Chef::Log.debug("Binding mode for #{service_name} not found on Supervisor API") "strict" end def get_service_group(service_details) service_details["service_group"].split(".").last rescue Chef::Log.debug("Service Group for #{service_name} not found on Supervisor API") "default" end def get_shutdown_timeout(service_details) service_details["pkg"]["shutdown_timeout"] rescue Chef::Log.debug("Shutdown Timeout for #{service_name} not found on Supervisor API") 8 end def get_health_check_interval(service_details) service_details["health_check_interval"]["secs"] rescue Chef::Log.debug("Health Check Interval for #{service_name} not found on Supervisor API") 30 end action :load, description: "(default action) runs `hab service load` to load and start the specified application service" do modified = false converge_if_changed :service_name do modified = true end converge_if_changed :strategy do modified = true end converge_if_changed :update_condition do modified = true end converge_if_changed :topology do modified = true end converge_if_changed :bldr_url do modified = true end converge_if_changed :channel do modified = true end converge_if_changed :bind do modified = true end converge_if_changed :binding_mode do modified = true end converge_if_changed :service_group do modified = true end converge_if_changed :shutdown_timeout do modified = true end converge_if_changed :health_check_interval do modified = true end options = svc_options if current_resource.loaded && modified Chef::Log.debug("Reloading #{current_resource.service_name} using --force due to parameter change") options << "--force" end unless current_resource.loaded && !modified execute "test" do command "hab svc load #{new_resource.service_name} #{options.join(" ")}" retry_delay 10 retries 5 end end end action :unload, description: "runs `hab service unload` to unload and stop the specified application service" do if current_resource.loaded execute "hab svc unload #{new_resource.service_name} #{svc_options.join(" ")}" wait_for_service_unloaded end end action :start, description: "runs `hab service start` to start the specified application service" do unless current_resource.loaded Chef::Log.fatal("No service named #{new_resource.service_name} is loaded on the Habitat supervisor") raise "No service named #{new_resource.service_name} is loaded on the Habitat supervisor" end execute "hab svc start #{new_resource.service_name} #{svc_options.join(" ")}" unless current_resource.running end action :stop, description: "runs `hab service stop` to stop the specified application service" do unless current_resource.loaded Chef::Log.fatal("No service named #{new_resource.service_name} is loaded on the Habitat supervisor") raise "No service named #{new_resource.service_name} is loaded on the Habitat supervisor" end if current_resource.running execute "hab svc stop #{new_resource.service_name} #{svc_options.join(" ")}" wait_for_service_stopped end end action :restart, description: "runs the `:stop` and then `:start` actions" do action_stop action_start end action :reload, description: "runs the `:unload` and then `:load` actions" do action_unload action_load end action_class do def svc_options opts = [] # certain options are only valid for specific `hab svc` subcommands. case action when :load opts.push(*new_resource.bind.map { |b| "--bind #{b}" }) if new_resource.bind opts << "--binding-mode #{new_resource.binding_mode}" opts << "--url #{new_resource.bldr_url}" if new_resource.bldr_url opts << "--channel #{new_resource.channel}" if new_resource.channel opts << "--group #{new_resource.service_group}" if new_resource.service_group opts << "--strategy #{new_resource.strategy}" if new_resource.strategy opts << "--update-condition #{new_resource.update_condition}" if new_resource.update_condition opts << "--topology #{new_resource.topology}" if new_resource.topology opts << "--health-check-interval #{new_resource.health_check_interval}" if new_resource.health_check_interval opts << "--shutdown-timeout #{new_resource.shutdown_timeout}" if new_resource.shutdown_timeout when :unload, :stop opts << "--shutdown-timeout #{new_resource.shutdown_timeout}" if new_resource.shutdown_timeout end opts << "--remote-sup #{new_resource.remote_sup}" if new_resource.remote_sup opts.map(&:split).flatten.compact end def wait_for_service_unloaded ruby_block "wait-for-service-unloaded" do block do raise "#{new_resource.service_name} still loaded" if service_loaded?(get_service_details(new_resource.service_name)) end retries get_shutdown_timeout(new_resource.service_name) + 1 retry_delay 1 end ruby_block "update current_resource" do block do current_resource.loaded = service_loaded?(get_service_details(new_resource.service_name)) end action :nothing subscribes :run, "ruby_block[wait-for-service-unloaded]", :immediately end end def wait_for_service_stopped ruby_block "wait-for-service-stopped" do block do raise "#{new_resource.service_name} still running" if service_up?(get_service_details(new_resource.service_name)) end retries get_shutdown_timeout(new_resource.service_name) + 1 retry_delay 1 ruby_block "update current_resource" do block do current_resource.running = service_up?(get_service_details(new_resource.service_name)) end action :nothing subscribes :run, "ruby_block[wait-for-service-stopped]", :immediately end end end end end end end