Welcome to mirror list, hosted at ThFree Co, Russian Federation.

state.rb « terraform « api « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: bdc9f9759706d4dd57a5c70bc1657a781caea3ae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# frozen_string_literal: true

require_dependency 'api/validations/validators/limit'

module API
  module Terraform
    class State < ::API::Base
      include ::Gitlab::Utils::StrongMemoize

      feature_category :infrastructure_as_code
      urgency :low

      default_format :json

      rescue_from(
        ::Terraform::RemoteStateHandler::StateDeletedError,
        ::ActiveRecord::RecordNotUnique,
        ::PG::UniqueViolation
      ) do |e|
        render_api_error!(e.message, 422)
      end

      STATE_NAME_URI_REQUIREMENTS = { name: API::NO_SLASH_URL_PART_REGEX }.freeze

      before do
        authenticate!
        authorize! :read_terraform_state, user_project

        increment_unique_values('p_terraform_state_api_unique_users', current_user.id)

        if Feature.enabled?(:route_hll_to_snowplow_phase2, user_project&.namespace)
          Gitlab::Tracking.event(
            'API::Terraform::State',
            'terraform_state_api_request',
            namespace: user_project&.namespace,
            user: current_user,
            project: user_project,
            label: 'redis_hll_counters.terraform.p_terraform_state_api_unique_users_monthly',
            context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
                                                               event: 'p_terraform_state_api_unique_users').to_context]
          )
        end
      end

      params do
        requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
      end

      resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
        namespace ':id/terraform/state/:name', requirements: STATE_NAME_URI_REQUIREMENTS do
          params do
            requires :name, type: String, desc: 'The name of a Terraform state'
            optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
          end

          helpers do
            def remote_state_handler
              ::Terraform::RemoteStateHandler.new(user_project, current_user, name: params[:name], lock_id: params[:ID])
            end

            def not_found_for_dots?
              Feature.disabled?(:allow_dots_on_tf_state_names) && params[:name].include?(".")
            end

            # Change the state name to behave like before, https://gitlab.com/gitlab-org/gitlab/-/merge_requests/105674
            # has been introduced. This behavior can be controlled via `allow_dots_on_tf_state_names` FF.
            # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106861
            def legacy_state_name!
              params[:name] = params[:name].split('.').first
            end
          end

          desc 'Get a Terraform state by its name' do
            detail 'Get a Terraform state by its name'
            success [
              { code: 200 },
              { code: 204, message: 'Empty state' }
            ]
            failure [
              { code: 403, message: 'Forbidden' },
              { code: 404, message: 'Not found' },
              { code: 422, message: 'Validation failure' }
            ]
            tags %w[terraform_state]
          end
          route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
          get do
            legacy_state_name! if not_found_for_dots?

            remote_state_handler.find_with_lock do |state|
              no_content! unless state.latest_file && state.latest_file.exists?

              env['api.format'] = :binary # this bypasses json serialization
              body state.latest_file.read
            end
          end

          desc 'Add a new Terraform state or update an existing one' do
            detail 'Add a new Terraform state or update an existing one'
            success [
              { code: 200 },
              { code: 204, message: 'No data provided' }
            ]
            failure [
              { code: 403, message: 'Forbidden' },
              { code: 422, message: 'Validation failure' },
              { code: 413, message: 'Request Entity Too Large' }
            ]
            tags %w[terraform_state]
          end
          route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
          post do
            authorize! :admin_terraform_state, user_project
            legacy_state_name! if not_found_for_dots?

            data = request.body.read
            no_content! if data.empty?

            max_state_size = Gitlab::CurrentSettings.max_terraform_state_size_bytes
            file_too_large! if max_state_size > 0 && data.size > max_state_size

            remote_state_handler.handle_with_lock do |state|
              state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial], build: current_authenticated_job)
            end

            body false
            status :ok
          end

          desc 'Delete a Terraform state of a certain name' do
            detail 'Delete a Terraform state of a certain name'
            success code: 200
            failure [
              { code: 403, message: 'Forbidden' },
              { code: 404, message: 'Not found' },
              { code: 422, message: 'Validation failure' }
            ]
            tags %w[terraform_state]
          end
          route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
          delete do
            authorize! :admin_terraform_state, user_project
            legacy_state_name! if not_found_for_dots?

            remote_state_handler.find_with_lock do |state|
              ::Terraform::States::TriggerDestroyService.new(state, current_user: current_user).execute
            end

            body false
            status :ok
          end

          desc 'Lock a Terraform state of a certain name' do
            detail 'Lock a Terraform state of a certain name'
            success code: 200
            failure [
              { code: 403, message: 'Forbidden' },
              { code: 404, message: 'Not found' },
              { code: 409, message: 'Conflict' },
              { code: 422, message: 'Validation failure' }
            ]
            tags %w[terraform_state]
          end
          route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
          params do
            requires :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
            requires :Operation, type: String, desc: 'Terraform operation'
            requires :Info, type: String, desc: 'Terraform info'
            requires :Who, type: String, desc: 'Terraform state lock owner'
            requires :Version, type: String, desc: 'Terraform version'
            requires :Created, type: String, desc: 'Terraform state lock timestamp'
            requires :Path, type: String, desc: 'Terraform path'
          end
          post '/lock' do
            not_found! if not_found_for_dots?

            authorize! :admin_terraform_state, user_project

            status_code = :ok
            lock_info = {
              'Operation' => params[:Operation],
              'Info' => params[:Info],
              'Version' => params[:Version],
              'Path' => params[:Path]
            }

            begin
              remote_state_handler.lock!
            rescue ::Terraform::RemoteStateHandler::StateLockedError
              status_code = :conflict
            end

            remote_state_handler.find_with_lock do |state|
              lock_info['ID'] = state.lock_xid
              lock_info['Who'] = state.locked_by_user.username
              lock_info['Created'] = state.locked_at

              env['api.format'] = :binary # this bypasses json serialization
              body lock_info.to_json
              status status_code
            end
          end

          desc 'Unlock a Terraform state of a certain name' do
            detail 'Unlock a Terraform state of a certain name'
            success code: 200
            failure [
              { code: 403, message: 'Forbidden' },
              { code: 404, message: 'Not found' },
              { code: 409, message: 'Conflict' },
              { code: 422, message: 'Validation failure' }
            ]
            tags %w[terraform_state]
          end
          route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
          params do
            optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
          end
          delete '/lock' do
            not_found! if not_found_for_dots?

            authorize! :admin_terraform_state, user_project

            remote_state_handler.unlock!
            status :ok
          rescue ::Terraform::RemoteStateHandler::StateLockedError
            status :conflict
          end
        end
      end
    end
  end
end