[go: up one dir, main page]

Skip to content

Make PushOptions a cross-domain framework

Problem

We are using push options as Hash object in many parts of the application and this has turned into a Primitive Obsession. Push options is more of a framework for passing data to GitLab backend via git push. Today we have various types of data (namespaces) of push options such as ci, merge_request, integrations, etc.

While groupsource code would own the framework (of passing data), each data namespace should be owned by various domains. The proposal below can be one way we could solve this in a modularized way.

Context

This issue is extracted from a code review where we introduced a CI-specific push options class rather than having specific parser logic defined in Git::BaseHooksService.

The following discussion from !186297 (merged) should be addressed:

  • @fabiopitino started a discussion:

    non-blocking: I just realized that we do have Gitlab::PushOptions object that that is used in PostReceiveService https://gitlab.com/gitlab-org/gitlab/-/blob/c5c3ed711c5f47a4d872ecd63ad86451b09be56e/app/services/post_receive_service.rb#L61 but later it's pushed to the PostReceive Sidekiq worker as JSON and from there on we use it as Hash.

    It would be great to have a SSoT for dealing with push options but it would require an even bigger refactoring because:

    • Gitlab::PushOptions expects options in Array form (as it comes from Gitaly).
    • The initializer of Gitlab::PushOptions is primarily a parser. The rest of the class is a basic access object.
    • We could have Gitlab::PushOptions to be a SSoT for serialized and deserialized options
    • Each domain could implement their own Push Options namespace (e.g. ci.*) and be responsible for it.
    • The groupsource code would be responsible for the general push options framework while each team owns their own namespace
    class Gitlab::PushOptions
      def self.fabricate(push_options)
        # handle: array (gitaly), hash/json, PushOptions object, nil
      end
    
      NAMESPACES = {
        ci: ::Ci::PushOptions, # each domain owns their own push options
        merge_request: ::MergeRequests::PushOptions,
        # ...
      }
    
      def initialize(data)
        @data = data
        @opts = {}
    
        NAMESPACES.each do |namespace, handler|
          @opts[namespace] = handler.new(data.fetch(namespace.to_sym, {})) 
        end
    
        @opts.each(&:validate!)
      end
    
      def ci
        @opts.fetch(:ci)
      end
    
      def merge_request
        @opts.fetch(:merge_request)
      end
    
      def as_json(*_args)
        @data.as_json
      end
    end
    
    class Gitlab::PushOptions::Namespace
      def initialize(data)
        @data = data
      end
    
      def validate!
        # use locally defined ALLOWED_OPTIONS and MULTI_VALUE_OPTIONS
      end
    end
    
    class Ci::PushOptions < Gitlab::PushOptions::Namespace
      ALLOWED_OPTIONS = [:skip, :variable, :input]
      MULTI_VALUE_OPTIONS = [:input, :variable]
    
      def skip?
        data[:skip].present?
      end
    
      def variables
        # ...
      end
    
      def inputs
        # ...
      end
    end
    
    ####
    # Usage:
    push_options = Gitlab::PushOptions.fabricate(data)
    push_options.ci.skip? # => false
    push_options.ci.inputs # => { foo: 'bar' }
    push_options.merge_request.labels # => ['foo', 'bar']
    push_options.secret_push_protection.skip_all? # => true
    push_options.as_json # => { ... }
Edited by Fabio Pitino