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

: we can remove this once there is a tenant_id field on modules #356

Open
github-actions bot opened this issue Aug 15, 2023 · 0 comments
Open

: we can remove this once there is a tenant_id field on modules #356

github-actions bot opened this issue Aug 15, 2023 · 0 comments
Assignees
Labels

Comments

@github-actions
Copy link

which will make this much simpler to filter

  1. grabs all the module ids in the systems of the provided org zone

  2. select distinct modules ids which are not logic modules (99)

to display the inherited settings

to display the inherited settings

get("/:id/settings", :settings) do

# TODO:: we can remove this once there is a tenant_id field on modules

    ###############################################################################################

    before_action :can_read, only: [:index, :show]
    before_action :can_write, only: [:create, :update, :destroy, :remove]

    before_action :check_admin, except: [:index, :state, :show, :ping]
    before_action :check_support, only: [:state, :show, :ping]

    ###############################################################################################

    @[AC::Route::Filter(:before_action, except: [:index, :create])]
    def find_current_module(id : String)
      Log.context.set(module_id: id)
      # Find will raise a 404 (not found) if there is an error
      @current_module = Model::Module.find!(id)
    end

    getter! current_module : Model::Module

    # Permissions
    ###############################################################################################

    @[AC::Route::Filter(:before_action, only: [:index])]
    def check_view_permissions
      return if user_support?

      # find the org zone
      authority = current_authority.as(Model::Authority)
      @org_zone_id = org_zone_id = authority.config["org_zone"]?.try(&.as_s?)
      raise Error::Forbidden.new unless org_zone_id

      access = check_access(current_user.groups, [org_zone_id])
      raise Error::Forbidden.new unless access.admin?
    end

    getter org_zone_id : String? = nil

    # Response helpers
    ###############################################################################################

    record ControlSystemDetails, name : String, zone_data : Array(Model::Zone) do
      include JSON::Serializable
    end

    record DriverDetails, name : String, description : String?, module_name : String? do
      include JSON::Serializable
    end

    # extend the ControlSystem model to handle our return values
    class Model::Module
      @[JSON::Field(key: "driver")]
      property driver_details : Api::Modules::DriverDetails? = nil
      property compiled : Bool? = nil
      @[JSON::Field(key: "control_system")]
      property control_system_details : Api::Modules::ControlSystemDetails? = nil
    end

    ###############################################################################################

    # return a list of modules configured on the cluster
    @[AC::Route::GET("/")]
    def index(
      @[AC::Param::Info(description: "only return modules updated before this time (unix epoch)")]
      as_of : Int64? = nil,
      @[AC::Param::Info(description: "only return modules running in this system (query params are ignored if this is provided)", example: "sys-1234")]
      control_system_id : String? = nil,
      @[AC::Param::Info(description: "only return modules with a particular connected state", example: "true")]
      connected : Bool? = nil,
      @[AC::Param::Info(description: "only return instances of this driver", example: "driver-1234")]
      driver_id : String? = nil,
      @[AC::Param::Info(description: "do not return logic modules (return only modules that can exist in multiple systems)", example: "true")]
      no_logic : Bool = false,
      @[AC::Param::Info(description: "return only running modules", example: "true")]
      running : Bool? = nil
    ) : Array(Model::Module)
      # if a system id is present we query the database directly
      if control_system_id
        cs = Model::ControlSystem.find!(control_system_id)
        # Include subset of association data with results
        results = Model::Module.find_all(cs.modules).compact_map do |mod|
          next if (driver = mod.driver).nil?

          # Most human readable module data is contained in driver
          mod.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
          mod.compiled = Api::Modules.driver_compiled?(mod, request_id)
          mod
        end.to_a

        set_collection_headers(results.size, Model::Module.table_name)

        return results
      end

      # we use Elasticsearch
      elastic = Model::Module.elastic
      query = elastic.query(search_params)
      query.minimum_should_match(1)

      # TODO:: we can remove this once there is a tenant_id field on modules
      # which will make this much simpler to filter
      if filter_zone_id = org_zone_id
        # we only want to show modules in use by systems that include this zone
        no_logic = true

        # find all the non-logic modules that this user can access
        # 1. grabs all the module ids in the systems of the provided org zone
        # 2. select distinct modules ids which are not logic modules (99)
        sql_query = %[
          WITH matching_rows AS (
            SELECT unnest(modules) AS module_id
            FROM sys
            WHERE $1 = ANY(zones)
          )

          SELECT ARRAY_AGG(DISTINCT m.module_id)
          FROM matching_rows m
          JOIN mod ON m.module_id = mod.id
          WHERE mod.role <> 99;
        ]

        module_ids = PgORM::Database.connection do |conn|
          conn.query_one(sql_query, args: [filter_zone_id], &.read(Array(String)))
        end

        query.must({
          "id" => module_ids,
        })
      end

      if no_logic
        query.must_not({"role" => [Model::Driver::Role::Logic.to_i]})
      end

      if driver_id
        query.filter({"driver_id" => [driver_id]})
      end

      unless connected.nil?
        query.filter({
          "ignore_connected" => [false],
          "connected"        => [connected],
        })
      end

      unless running.nil?
        query.should({"running" => [running]})
      end

      if as_of
        query.range({
          "updated_at" => {
            :lte => as_of,
          },
        })
      end

      query.has_parent(parent: Model::Driver, parent_index: Model::Driver.table_name)

      search_results = paginate_results(elastic, query)

      # Include subset of association data with results
      search_results.compact_map do |d|
        sys = d.control_system
        driver = d.driver
        next unless driver

        # Include control system on Logic modules so it is possible
        # to display the inherited settings
        sys_field = if sys
                      ControlSystemDetails.new(sys.name, Model::Zone.find_all(sys.zones).to_a)
                    else
                      nil
                    end

        d.control_system_details = sys_field
        d.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
        d
      end
    end

    # return the details of a module
    @[AC::Route::GET("/:id")]
    def show(
      @[AC::Param::Info(description: "return the driver details along with the module?", example: "true")]
      complete : Bool = false
    ) : Model::Module
      if complete && (driver = current_module.driver)
        current_module.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
        current_module
      else
        current_module
      end
    end

    # update the details of a module
    @[AC::Route::PATCH("/:id", body: :mod)]
    @[AC::Route::PUT("/:id", body: :mod)]
    def update(mod : Model::Module) : Model::Module
      current = current_module
      current.assign_attributes(mod)
      raise Error::ModelValidation.new(current.errors) unless current.save

      if driver = current.driver
        current.driver_details = DriverDetails.new(driver.name, driver.description, driver.module_name)
      end
      current
    end

    # add a new module / instance of a driver
    @[AC::Route::POST("/", body: :mod, status_code: HTTP::Status::CREATED)]
    def create(mod : Model::Module) : Model::Module
      raise Error::ModelValidation.new(mod.errors) unless mod.save
      mod
    end

    # remove a module
    @[AC::Route::DELETE("/:id", status_code: HTTP::Status::ACCEPTED)]
    def destroy : Nil
      current_module.destroy
    end

    # Receive the collated settings for a module
    @[AC::Route::GET("/:id/settings")]
    def settings : Array(PlaceOS::Model::Settings)
      Api::Settings.collated_settings(current_user, current_module)
    end

    # Starts a module
    @[AC::Route::POST("/:id/start")]
    def start : Nil
      return if current_module.running == true
      current_module.update_fields(running: true)

      # Changes cleared on a successful update
      if current_module.running_changed?
        Log.error { {controller: "Modules", action: "start", module_id: current_module.id, event: "failed"} }
        raise "failed to update database to start module #{current_module.id}"
      end
    end

    # Stops a module
    @[AC::Route::POST("/:id/stop")]
    def stop : Nil
      return unless current_module.running
      current_module.update_fields(running: false)

      # Changes cleared on a successful update
      if current_module.running_changed?
        Log.error { {controller: "Modules", action: "stop", module_id: current_module.id, event: "failed"} }
        raise "failed to update database to stop module #{current_module.id}"
      end
    end

    # Executes a command on a module
    # The `/systems/` route can be used to introspect modules for the list of methods and argument requirements
    @[AC::Route::POST("/:id/exec/:method", body: :args)]
    def execute(
      id : String,
      @[AC::Param::Info(description: "the name of the methodm we want to execute")]
      method : String,
      @[AC::Param::Info(description: "the arguments we want to provide to the method")]
      args : Array(JSON::Any)
    ) : Nil
      sys_id = current_module.control_system_id || ""

      result, status_code = Driver::Proxy::RemoteDriver.new(
        module_id: id,
        sys_id: sys_id,
        module_name: current_module.name,
        discovery: self.class.core_discovery,
        user_id: current_user.id,
      ) { |module_id|
        Model::Module.find!(module_id).edge_id.as(String)
      }.exec(
        security: driver_clearance(user_token),
        function: method,
        args: args,
        request_id: request_id,
      )

      # customise the response based on the execute results
      response.content_type = "application/json"
      render text: result, status: status_code
    rescue e : Driver::Proxy::RemoteDriver::Error
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant