diff --git a/db/migrate/20250114194544_add_user_provided_to_workspace_variables.rb b/db/migrate/20250114194544_add_user_provided_to_workspace_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..07c3759b69da9830080ed28dd6d6ef7f08772377 --- /dev/null +++ b/db/migrate/20250114194544_add_user_provided_to_workspace_variables.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddUserProvidedToWorkspaceVariables < Gitlab::Database::Migration[2.2] + milestone '17.9' + + def change + add_column :workspace_variables, :user_provided, :boolean, null: false, default: false, if_not_exists: true + end +end diff --git a/db/migrate/20250114194610_backfill_user_provided_workspace_variables.rb b/db/migrate/20250114194610_backfill_user_provided_workspace_variables.rb new file mode 100644 index 0000000000000000000000000000000000000000..187625cea7b6ac6ae717a1b71b9ab670d4ef5cf7 --- /dev/null +++ b/db/migrate/20250114194610_backfill_user_provided_workspace_variables.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class BackfillUserProvidedWorkspaceVariables < Gitlab::Database::Migration[2.2] + milestone '17.9' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class WorkspaceVariable < MigrationRecord + self.table_name = :workspace_variables + + include EachBatch + end + + BATCH_SIZE = 100 + + # Since file type is not supported for user variables + VARIABLE_ENV_TYPE = 0 + + # Internal workspace variables that get created on workspace creation + # Reference: /ee/lib/remote_development/workspace_operations/create/workspace_variables.rb + WORKSPACE_INTERNAL_VARIABLES = %w[ + GIT_CONFIG_COUNT + GIT_CONFIG_KEY_0 + GIT_CONFIG_VALUE_0 + GIT_CONFIG_KEY_1 + GIT_CONFIG_VALUE_1 + GIT_CONFIG_KEY_2 + GIT_CONFIG_VALUE_2 + GL_GIT_CREDENTIAL_STORE_FILE_PATH + GL_TOKEN_FILE_PATH + GL_WORKSPACE_DOMAIN_TEMPLATE + GL_EDITOR_EXTENSIONS_GALLERY_SERVICE_URL + GL_EDITOR_EXTENSIONS_GALLERY_ITEM_URL + GL_EDITOR_EXTENSIONS_GALLERY_RESOURCE_URL_TEMPLATE + GITLAB_WORKFLOW_INSTANCE_URL + GITLAB_WORKFLOW_TOKEN_FILE + ].freeze + + def up + WorkspaceVariable.reset_column_information + + WorkspaceVariable.each_batch(of: BATCH_SIZE) do |batch| + batch.where(variable_type: VARIABLE_ENV_TYPE).where.not(key: WORKSPACE_INTERNAL_VARIABLES) + .update_all(user_provided: true) + end + end + + def down + WorkspaceVariable.reset_column_information + + # Column is NOT NULL DEFAULT 0, so setting back to default + WorkspaceVariable.update_all(user_provided: false) + end +end diff --git a/db/schema_migrations/20250114194544 b/db/schema_migrations/20250114194544 new file mode 100644 index 0000000000000000000000000000000000000000..1136a615f5baff6c0d72d1bd9cb87f6550fefe7d --- /dev/null +++ b/db/schema_migrations/20250114194544 @@ -0,0 +1 @@ +79ded29b939ad063a58275fec52191c01161d48b89758b05f3dc3f15c41f018d \ No newline at end of file diff --git a/db/schema_migrations/20250114194610 b/db/schema_migrations/20250114194610 new file mode 100644 index 0000000000000000000000000000000000000000..04cc0d3f4f3735251cd85b97a9281dbcb392ae1f --- /dev/null +++ b/db/schema_migrations/20250114194610 @@ -0,0 +1 @@ +e57acf78acb8d7c507b2e4d611be22d6741f4a0e09970776bb86f2aca266b542 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ec51c6336aeec41ed1d7831acd5447f4937514b0..ce302baf8b52e491ffdca59fd920a9e3a631f9d4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23254,6 +23254,7 @@ CREATE TABLE workspace_variables ( encrypted_value bytea NOT NULL, encrypted_value_iv bytea NOT NULL, project_id bigint, + user_provided boolean DEFAULT false NOT NULL, CONSTRAINT check_5545042100 CHECK ((char_length(key) <= 255)), CONSTRAINT check_ed95da8691 CHECK ((project_id IS NOT NULL)) ); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3d68d696080446f1c36b95af6a9da8ef9681ec5d..406cedcdcf5f2b92a372c5ebcc08b8951e2fd232 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -18344,6 +18344,29 @@ The edge type for [`Workspace`](#workspace). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`Workspace`](#workspace) | The item at the end of the edge. | +#### `WorkspaceVariableConnection` + +The connection type for [`WorkspaceVariable`](#workspacevariable). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[WorkspaceVariableEdge]`](#workspacevariableedge) | A list of edges. | +| `nodes` | [`[WorkspaceVariable]`](#workspacevariable) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `WorkspaceVariableEdge` + +The edge type for [`WorkspaceVariable`](#workspacevariable). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`WorkspaceVariable`](#workspacevariable) | The item at the end of the edge. | + ## Object types Object types represent the resources that the GitLab GraphQL API can return. @@ -21308,7 +21331,7 @@ four standard [pagination arguments](#pagination-arguments): | ---- | ---- | ----------- | | `actualStates` | [`[String!]`](#string) | Filter workspaces by actual states. | | `ids` | [`[RemoteDevelopmentWorkspaceID!]`](#remotedevelopmentworkspaceid) | Filter workspaces by workspace GlobalIDs. For example, `["gid://gitlab/RemoteDevelopment::Workspace/1"]`. | -| `projectIds` | [`[ProjectID!]`](#projectid) | Filter workspaces by project GlobalID. | +| `projectIds` | [`[ProjectID!]`](#projectid) | Filter workspaces by project GlobalIDs. | ### `ClusterAgentActivityEvent` @@ -38476,8 +38499,24 @@ Represents a remote development workspace. | `updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable workspace property. | | `url` | [`String!`](#string) | URL of the workspace. | | `user` | [`UserCore!`](#usercore) | Owner of the workspace. | +| `workspaceVariables` **{warning-solid}** | [`WorkspaceVariableConnection`](#workspacevariableconnection) | **Introduced** in GitLab 17.9. **Status**: Experiment. User defined variables associated with the workspace. | | `workspacesAgentConfigVersion` **{warning-solid}** | [`Int!`](#int) | **Introduced** in GitLab 17.6. **Status**: Experiment. Version of the associated WorkspacesAgentConfig for the workspace. | +### `WorkspaceVariable` + +Represents a remote development workspace variable. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `createdAt` | [`Time!`](#time) | Timestamp of when the workspace variable was created. | +| `id` | [`RemoteDevelopmentWorkspaceVariableID!`](#remotedevelopmentworkspacevariableid) | Global ID of the workspace variable. | +| `key` | [`String`](#string) | Name of the workspace variable. | +| `updatedAt` | [`Time!`](#time) | Timestamp of when the workspace variable was updated. | +| `value` | [`String`](#string) | Value of the workspace variable. | +| `variableType` | [`WorkspaceVariableType`](#workspacevariabletype) | Type of the workspace variable. | + ### `WorkspacesAgentConfig` Represents a workspaces agent config. @@ -42177,6 +42216,14 @@ Enum for the type of the variable to be injected in a workspace. | ----- | ----------- | | `ENVIRONMENT` | Name type. | +### `WorkspaceVariableType` + +Enum for the type of the variable injected in a workspace. + +| Value | Description | +| ----- | ----------- | +| `ENVIRONMENT` | Environment type. | + ## Scalar types Scalar values are atomic values, and do not have fields of their own. @@ -43149,6 +43196,12 @@ A `RemoteDevelopmentWorkspaceID` is a global ID. It is encoded as a string. An example `RemoteDevelopmentWorkspaceID` is: `"gid://gitlab/RemoteDevelopment::Workspace/1"`. +### `RemoteDevelopmentWorkspaceVariableID` + +A `RemoteDevelopmentWorkspaceVariableID` is a global ID. It is encoded as a string. + +An example `RemoteDevelopmentWorkspaceVariableID` is: `"gid://gitlab/RemoteDevelopment::WorkspaceVariable/1"`. + ### `RemoteDevelopmentWorkspacesAgentConfigID` A `RemoteDevelopmentWorkspacesAgentConfigID` is a global ID. It is encoded as a string. @@ -45634,6 +45687,7 @@ Attributes for defining a variable to be injected in a workspace. | Name | Type | Description | | ---- | ---- | ----------- | -| `key` | [`String!`](#string) | Key of the variable. | -| `type` | [`WorkspaceVariableInputType!`](#workspacevariableinputtype) | Type of the variable to be injected in a workspace. | +| `key` | [`String!`](#string) | Name of the workspace variable. | +| `type` **{warning-solid}** | [`WorkspaceVariableInputType`](#workspacevariableinputtype) | **Deprecated:** Use `variableType` instead. Deprecated in GitLab 17.9. | | `value` | [`String!`](#string) | Value of the variable. | +| `variableType` | [`WorkspaceVariableType`](#workspacevariabletype) | Type of the variable to be injected in a workspace. | diff --git a/ee/app/graphql/ee/types/query_type.rb b/ee/app/graphql/ee/types/query_type.rb index feb21cf019d43c37c8dd3cfd663e7102a3d24b55..90669245b3e1ef742bcacce19392b97f597ddf1e 100644 --- a/ee/app/graphql/ee/types/query_type.rb +++ b/ee/app/graphql/ee/types/query_type.rb @@ -98,7 +98,7 @@ module QueryType field :workspaces, ::Types::RemoteDevelopment::WorkspaceType.connection_type, null: true, - resolver: ::Resolvers::RemoteDevelopment::AdminWorkspacesResolver, + resolver: ::Resolvers::RemoteDevelopment::WorkspacesAdminResolver, description: 'Find workspaces across the entire instance. This field is only available to instance admins, ' \ 'it will return an empty result for all non-admins.' field :instance_external_audit_event_destinations, diff --git a/ee/app/graphql/resolvers/remote_development/cluster_agent/workspaces_resolver.rb b/ee/app/graphql/resolvers/remote_development/cluster_agent/workspaces_resolver.rb index 68c04db7e4506d429e7f0c9782f0cec655d430d8..f5556295e06a119f8d379b33df719830d50bc1b4 100644 --- a/ee/app/graphql/resolvers/remote_development/cluster_agent/workspaces_resolver.rb +++ b/ee/app/graphql/resolvers/remote_development/cluster_agent/workspaces_resolver.rb @@ -3,40 +3,24 @@ module Resolvers module RemoteDevelopment module ClusterAgent - class WorkspacesResolver < ::Resolvers::BaseResolver - include ResolvesIds + class WorkspacesResolver < WorkspacesBaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource type Types::RemoteDevelopment::WorkspaceType.connection_type, null: true authorize :admin_cluster authorizes_object! - argument :ids, [::Types::GlobalIDType[::RemoteDevelopment::Workspace]], - required: false, - description: - 'Filter workspaces by workspace GlobalIDs. For example, `["gid://gitlab/RemoteDevelopment::Workspace/1"]`.' - - argument :project_ids, [::Types::GlobalIDType[Project]], - required: false, - description: 'Filter workspaces by project GlobalID.' - - argument :actual_states, [GraphQL::Types::String], - required: false, - description: 'Filter workspaces by actual states.' - alias_method :agent, :object - def resolve(**args) - unless License.feature_available?(:remote_development) - raise_resource_not_available_error! "'remote_development' licensed feature is not available" - end - - ::RemoteDevelopment::WorkspacesFinder.execute( - current_user: current_user, - agent_ids: [agent.id], - ids: resolve_ids(args[:ids]).map(&:to_i), - project_ids: resolve_ids(args[:project_ids]).map(&:to_i), - actual_states: args[:actual_states] || [] + def resolve_with_lookahead(**args) + apply_lookahead( + ::RemoteDevelopment::WorkspacesFinder.execute( + current_user: current_user, + agent_ids: [agent.id], + ids: resolve_ids(args[:ids]).map(&:to_i), + project_ids: resolve_ids(args[:project_ids]).map(&:to_i), + actual_states: args[:actual_states] || [] + ) ) end end diff --git a/ee/app/graphql/resolvers/remote_development/admin_workspaces_resolver.rb b/ee/app/graphql/resolvers/remote_development/workspaces_admin_resolver.rb similarity index 55% rename from ee/app/graphql/resolvers/remote_development/admin_workspaces_resolver.rb rename to ee/app/graphql/resolvers/remote_development/workspaces_admin_resolver.rb index ff856113f963775ea48e486418e0b85e06f95931..3713040c94e0401db68a61c2769f37bcdbecd697 100644 --- a/ee/app/graphql/resolvers/remote_development/admin_workspaces_resolver.rb +++ b/ee/app/graphql/resolvers/remote_development/workspaces_admin_resolver.rb @@ -2,9 +2,7 @@ module Resolvers module RemoteDevelopment - class AdminWorkspacesResolver < ::Resolvers::BaseResolver - include ResolvesIds - + class WorkspacesAdminResolver < WorkspacesBaseResolver # NOTE: We are intentionally not including Gitlab::Graphql::Authorize::AuthorizeResource, because this resolver # is currently only authorized at the instance admin level for all workspaces in the instance via the # `:read_all_workspaces` ability, so it's not necessary (or performant) to authorize individual workspaces. @@ -14,19 +12,10 @@ class AdminWorkspacesResolver < ::Resolvers::BaseResolver type Types::RemoteDevelopment::WorkspaceType.connection_type, null: true - argument :ids, [::Types::GlobalIDType[::RemoteDevelopment::Workspace]], - required: false, - description: - 'Filter workspaces by workspace GlobalIDs. For example, `["gid://gitlab/RemoteDevelopment::Workspace/1"]`.' - argument :user_ids, [::Types::GlobalIDType[Project]], required: false, description: 'Filter workspaces by user GlobalIDs.' - argument :project_ids, [::Types::GlobalIDType[Project]], - required: false, - description: 'Filter workspaces by project GlobalIDs.' - argument :agent_ids, [::Types::GlobalIDType[::Clusters::Agent]], required: false, description: 'Filter workspaces by agent GlobalIDs.' @@ -36,28 +25,19 @@ class AdminWorkspacesResolver < ::Resolvers::BaseResolver deprecated: { reason: 'Use actual_states instead', milestone: '16.7' }, description: 'Filter workspaces by actual states.' - argument :actual_states, [GraphQL::Types::String], - required: false, - description: 'Filter workspaces by actual states.' - - def resolve(**args) - # rubocop:disable Graphql/ResourceNotAvailableError -- We are intentionally not including Gitlab::Graphql::Authorize::AuthorizeResource - see note at top of class - unless License.feature_available?(:remote_development) - raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, - "'remote_development' licensed feature is not available" - end - # rubocop:enable Graphql/ResourceNotAvailableError - + def resolve_with_lookahead(**args) return ::RemoteDevelopment::Workspace.none unless can_read_all_workspaces? begin - ::RemoteDevelopment::WorkspacesFinder.execute( - current_user: current_user, - ids: resolve_ids(args[:ids]).map(&:to_i), - user_ids: resolve_ids(args[:user_ids]).map(&:to_i), - project_ids: resolve_ids(args[:project_ids]).map(&:to_i), - agent_ids: resolve_ids(args[:agent_ids]).map(&:to_i), - actual_states: args[:actual_states] || args[:include_actual_states] || [] + apply_lookahead( + ::RemoteDevelopment::WorkspacesFinder.execute( + current_user: current_user, + ids: resolve_ids(args[:ids]).map(&:to_i), + user_ids: resolve_ids(args[:user_ids]).map(&:to_i), + project_ids: resolve_ids(args[:project_ids]).map(&:to_i), + agent_ids: resolve_ids(args[:agent_ids]).map(&:to_i), + actual_states: args[:actual_states] || args[:include_actual_states] || [] + ) ) rescue ArgumentError => e raise ::Gitlab::Graphql::Errors::ArgumentError, e.message diff --git a/ee/app/graphql/resolvers/remote_development/workspaces_base_resolver.rb b/ee/app/graphql/resolvers/remote_development/workspaces_base_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c93bb0bc72810ace9235dda29d566680b1ec76d --- /dev/null +++ b/ee/app/graphql/resolvers/remote_development/workspaces_base_resolver.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Resolvers + module RemoteDevelopment + class WorkspacesBaseResolver < ::Resolvers::BaseResolver + include ResolvesIds + include LooksAhead + + extras [:lookahead] + + type Types::RemoteDevelopment::WorkspaceType.connection_type, null: true + + argument :ids, [::Types::GlobalIDType[::RemoteDevelopment::Workspace]], + required: false, + description: + 'Filter workspaces by workspace GlobalIDs. For example, `["gid://gitlab/RemoteDevelopment::Workspace/1"]`.' + + argument :project_ids, [::Types::GlobalIDType[Project]], + required: false, + description: 'Filter workspaces by project GlobalIDs.' + + argument :actual_states, [GraphQL::Types::String], + required: false, + description: 'Filter workspaces by actual states.' + + def ready?(**args) + # rubocop:disable Graphql/ResourceNotAvailableError -- Gitlab::Graphql::Authorize::AuthorizeResource is not included + unless License.feature_available?(:remote_development) + raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, + "'remote_development' licensed feature is not available" + end + # rubocop:enable Graphql/ResourceNotAvailableError + + super + end + + def preloads + { + user_provided_workspace_variables: [:user_provided_workspace_variables] + } + end + end + end +end diff --git a/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb b/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb index c8fd4f3d76efee4d10af6fe1c8a306c09ee20660..078f7c3979f974ced4e1856d7168b7fbc8417121 100644 --- a/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb +++ b/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb @@ -2,21 +2,11 @@ module Resolvers module RemoteDevelopment - class WorkspacesResolver < ::Resolvers::BaseResolver - include ResolvesIds + class WorkspacesResolver < WorkspacesBaseResolver include Gitlab::Graphql::Authorize::AuthorizeResource type Types::RemoteDevelopment::WorkspaceType.connection_type, null: true - argument :ids, [::Types::GlobalIDType[::RemoteDevelopment::Workspace]], - required: false, - description: - 'Filter workspaces by workspace GlobalIDs. For example, `["gid://gitlab/RemoteDevelopment::Workspace/1"]`.' - - argument :project_ids, [::Types::GlobalIDType[Project]], - required: false, - description: 'Filter workspaces by project GlobalIDs.' - argument :agent_ids, [::Types::GlobalIDType[::Clusters::Agent]], required: false, description: 'Filter workspaces by agent GlobalIDs.' @@ -26,24 +16,18 @@ class WorkspacesResolver < ::Resolvers::BaseResolver deprecated: { reason: 'Use actual_states instead', milestone: '16.7' }, description: 'Filter workspaces by actual states.' - argument :actual_states, [GraphQL::Types::String], - required: false, - description: 'Filter workspaces by actual states.' - - def resolve(**args) - unless License.feature_available?(:remote_development) - raise_resource_not_available_error! "'remote_development' licensed feature is not available" - end - + def resolve_with_lookahead(**args) # noinspection RubyNilAnalysis - This is because the superclass #current_user uses #[], which can return nil # TODO: Change the superclass to use context.fetch(:current_user) instead of context[:current_user] - ::RemoteDevelopment::WorkspacesFinder.execute( - current_user: current_user, - user_ids: [current_user.id], - ids: resolve_ids(args[:ids]).map(&:to_i), - project_ids: resolve_ids(args[:project_ids]).map(&:to_i), - agent_ids: resolve_ids(args[:agent_ids]).map(&:to_i), - actual_states: args[:actual_states] || args[:include_actual_states] || [] + apply_lookahead( + ::RemoteDevelopment::WorkspacesFinder.execute( + current_user: current_user, + user_ids: [current_user.id], + ids: resolve_ids(args[:ids]).map(&:to_i), + project_ids: resolve_ids(args[:project_ids]).map(&:to_i), + agent_ids: resolve_ids(args[:agent_ids]).map(&:to_i), + actual_states: args[:actual_states] || args[:include_actual_states] || [] + ) ) end end diff --git a/ee/app/graphql/types/remote_development/workspace_type.rb b/ee/app/graphql/types/remote_development/workspace_type.rb index 503ec2459d63864d7a2c0cead4c0417c243e646c..1a6f5a3135dbc2dfa168e7e3a8b16cccf3ba2c54 100644 --- a/ee/app/graphql/types/remote_development/workspace_type.rb +++ b/ee/app/graphql/types/remote_development/workspace_type.rb @@ -101,6 +101,12 @@ class WorkspaceType < ::Types::BaseObject field :updated_at, Types::TimeType, null: false, description: 'Timestamp of the last update to any mutable workspace property.' + field :workspace_variables, + ::Types::RemoteDevelopment::WorkspaceVariableType.connection_type, + null: true, + experiment: { milestone: '17.9' }, + description: 'User defined variables associated with the workspace.', method: :user_provided_workspace_variables + def project_id "gid://gitlab/Project/#{object.project_id}" end diff --git a/ee/app/graphql/types/remote_development/workspace_variable_input.rb b/ee/app/graphql/types/remote_development/workspace_variable_input.rb index 349bceaaac60600984d11ce07e1579aa9029e18f..6238f566826fb8e35801996944e024c2660a551d 100644 --- a/ee/app/graphql/types/remote_development/workspace_variable_input.rb +++ b/ee/app/graphql/types/remote_development/workspace_variable_input.rb @@ -9,14 +9,23 @@ class WorkspaceVariableInput < BaseInputObject # do not allow empty values. also validate that the key contains only alphanumeric characters, -, _ or . # https://kubernetes.io/docs/concepts/configuration/secret/#restriction-names-data argument :key, GraphQL::Types::String, - description: 'Key of the variable.', + description: 'Name of the workspace variable.', validates: { allow_blank: false, format: { with: /\A[a-zA-Z0-9\-_.]+\z/, message: 'must contain only alphanumeric characters, -, _ or .' } } argument :type, Types::RemoteDevelopment::WorkspaceVariableInputTypeEnum, - description: 'Type of the variable to be injected in a workspace.' + required: false, + default_value: Types::RemoteDevelopment::WorkspaceVariableInputTypeEnum.environment, + replace_null_with_default: true, + description: 'Type of the variable to be injected in a workspace.', + deprecated: { reason: 'Use `variableType` instead', milestone: '17.9' } argument :value, GraphQL::Types::String, description: 'Value of the variable.' + argument :variable_type, Types::RemoteDevelopment::WorkspaceVariableTypeEnum, + required: false, + default_value: Types::RemoteDevelopment::WorkspaceVariableTypeEnum.environment, + replace_null_with_default: true, + description: 'Type of the variable to be injected in a workspace.' end end end diff --git a/ee/app/graphql/types/remote_development/workspace_variable_input_type_enum.rb b/ee/app/graphql/types/remote_development/workspace_variable_input_type_enum.rb index 88de95f8c275868a6c2f4dd48e6ff0561748a774..c01fd833febed79be0548fc35452d32b5138f738 100644 --- a/ee/app/graphql/types/remote_development/workspace_variable_input_type_enum.rb +++ b/ee/app/graphql/types/remote_development/workspace_variable_input_type_enum.rb @@ -10,6 +10,10 @@ class WorkspaceVariableInputTypeEnum < BaseEnum ::RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES_FOR_GRAPHQL, description: "#{%(name).capitalize} type." ) + + def self.environment + enum[:environment] + end end end end diff --git a/ee/app/graphql/types/remote_development/workspace_variable_type.rb b/ee/app/graphql/types/remote_development/workspace_variable_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..3f31a3037686998530b95b0b5f33b69391b81ea9 --- /dev/null +++ b/ee/app/graphql/types/remote_development/workspace_variable_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module RemoteDevelopment + class WorkspaceVariableType < ::Types::BaseObject + graphql_name 'WorkspaceVariable' + description 'Represents a remote development workspace variable' + + authorize :read_workspace_variable + + field :id, ::Types::GlobalIDType[::RemoteDevelopment::WorkspaceVariable], + null: false, description: 'Global ID of the workspace variable.' + + field :key, GraphQL::Types::String, + null: true, description: 'Name of the workspace variable.' + + field :value, GraphQL::Types::String, + null: true, description: 'Value of the workspace variable.' + + field :variable_type, Types::RemoteDevelopment::WorkspaceVariableTypeEnum, + null: true, description: 'Type of the workspace variable.' + + field :created_at, Types::TimeType, + null: false, description: 'Timestamp of when the workspace variable was created.' + + field :updated_at, Types::TimeType, + null: false, description: 'Timestamp of when the workspace variable was updated.' + end + end +end diff --git a/ee/app/graphql/types/remote_development/workspace_variable_type_enum.rb b/ee/app/graphql/types/remote_development/workspace_variable_type_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..17114b61d747f70d8df027d6ef1427af2718ed73 --- /dev/null +++ b/ee/app/graphql/types/remote_development/workspace_variable_type_enum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module RemoteDevelopment + class WorkspaceVariableTypeEnum < BaseEnum + graphql_name 'WorkspaceVariableType' + description 'Enum for the type of the variable injected in a workspace.' + + ::RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES.slice(:environment).each do |name, value| + value name.to_s.upcase, value: value, description: "#{name.to_s.capitalize} type." + end + + def self.environment + enum[:environment] + end + end + end +end diff --git a/ee/app/models/remote_development/workspace.rb b/ee/app/models/remote_development/workspace.rb index 1acb86e716c7d1e9b126e379d2ce82a8dec0e63f..140669c53b39d78ac4afa4726cc0b5ce91b6b98f 100644 --- a/ee/app/models/remote_development/workspace.rb +++ b/ee/app/models/remote_development/workspace.rb @@ -23,6 +23,10 @@ class Workspace < ApplicationRecord # noinspection RailsParamDefResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31542 has_one :remote_development_agent_config, through: :agent, source: :remote_development_agent_config has_many :workspace_variables, class_name: 'RemoteDevelopment::WorkspaceVariable', inverse_of: :workspace + # Currently we only support :environment type for user provided variables + has_many :user_provided_workspace_variables, -> { + user_provided.with_variable_type_environment.order_id_desc + }, class_name: 'RemoteDevelopment::WorkspaceVariable', inverse_of: :workspace validates :user, presence: true validates :agent, presence: true diff --git a/ee/app/models/remote_development/workspace_variable.rb b/ee/app/models/remote_development/workspace_variable.rb index 30466b091159b679cd1d873ad6ea4083d59f9529..890ce302128cacc5e5fb1cb03a60e7012f6e0155 100644 --- a/ee/app/models/remote_development/workspace_variable.rb +++ b/ee/app/models/remote_development/workspace_variable.rb @@ -2,6 +2,8 @@ module RemoteDevelopment class WorkspaceVariable < ApplicationRecord + include Sortable + belongs_to :workspace, class_name: 'RemoteDevelopment::Workspace', inverse_of: :workspace_variables validates :variable_type, presence: true, inclusion: { @@ -19,6 +21,13 @@ class WorkspaceVariable < ApplicationRecord where(variable_type: RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES[:file]) } + scope :by_workspace_ids, ->(ids) { where(workspace_id: ids) } + scope :by_project_ids, ->(ids) { where(project_id: ids) } + + scope :user_provided, -> { + where(user_provided: true) + } + attr_encrypted :value, mode: :per_attribute_iv, key: ::Settings.attr_encrypted_db_key_base_32, diff --git a/ee/app/policies/remote_development/workspace_variable_policy.rb b/ee/app/policies/remote_development/workspace_variable_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..35f057527f0e2f1fdd04d7b50e9d1866bdceabb2 --- /dev/null +++ b/ee/app/policies/remote_development/workspace_variable_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class WorkspaceVariablePolicy < BasePolicy + condition(:can_read_workspace) { can?(:read_workspace, @subject.workspace) } + + rule { can_read_workspace }.enable :read_workspace_variable + end +end diff --git a/ee/lib/remote_development/workspace_operations/create/workspace_variables.rb b/ee/lib/remote_development/workspace_operations/create/workspace_variables.rb index 3a4ade8e84986ca53bb285d9c74f4bebd1999e77..4d65e083a731855803217cbd32c5982353ab4502 100644 --- a/ee/lib/remote_development/workspace_operations/create/workspace_variables.rb +++ b/ee/lib/remote_development/workspace_operations/create/workspace_variables.rb @@ -44,7 +44,7 @@ def self.variables( resource_url_template: String => vscode_extensions_gallery_resource_url_template, } - static_variables = [ + internal_variables = [ { key: File.basename(RemoteDevelopment::WorkspaceOperations::FileMounts::GITLAB_TOKEN_FILE), value: personal_access_token_value, @@ -155,11 +155,12 @@ def self.variables( key: variable.fetch(:key), value: variable.fetch(:value), variable_type: variable.fetch(:type), + user_provided: true, workspace_id: workspace_id } end - static_variables + user_provided_variables + internal_variables + user_provided_variables end end end diff --git a/ee/spec/factories/remote_development/workspace_variables.rb b/ee/spec/factories/remote_development/workspace_variables.rb index 04b4ed3dcbffb188d716e18eefe50fd1efbf1095..a4b50e6d09b1a3bbc57350b44b099f5ee5082383 100644 --- a/ee/spec/factories/remote_development/workspace_variables.rb +++ b/ee/spec/factories/remote_development/workspace_variables.rb @@ -6,6 +6,7 @@ key { 'my_key' } value { 'my_value' } + user_provided { false } variable_type { RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES[:file] } end end diff --git a/ee/spec/graphql/types/remote_development/workspace_type_spec.rb b/ee/spec/graphql/types/remote_development/workspace_type_spec.rb index 973a9109514834d4503614075d04e7f7e0144695..0ce217e3e87cf1d436192a4ee7712801cd9f39a7 100644 --- a/ee/spec/graphql/types/remote_development/workspace_type_spec.rb +++ b/ee/spec/graphql/types/remote_development/workspace_type_spec.rb @@ -10,6 +10,7 @@ url editor devfile_ref devfile_path devfile_web_url devfile processed_devfile project_ref deployment_resource_version desired_config_generator_version workspaces_agent_config_version force_include_all_resources created_at updated_at + workspace_variables ] end diff --git a/ee/spec/graphql/types/remote_development/workspace_variable_input_spec.rb b/ee/spec/graphql/types/remote_development/workspace_variable_input_spec.rb index d78c46f788f1da249a71bdd74f3cf9e097c21b64..619fba35dd976cdd88853183cdd0a29c06a59ad3 100644 --- a/ee/spec/graphql/types/remote_development/workspace_variable_input_spec.rb +++ b/ee/spec/graphql/types/remote_development/workspace_variable_input_spec.rb @@ -8,6 +8,7 @@ key type value + variableType ] end diff --git a/ee/spec/graphql/types/remote_development/workspace_variable_type_spec.rb b/ee/spec/graphql/types/remote_development/workspace_variable_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8ac3350b5b852991863a78c7b09ff7846911c1b1 --- /dev/null +++ b/ee/spec/graphql/types/remote_development/workspace_variable_type_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['WorkspaceVariable'], feature_category: :workspaces do + let(:fields) do + %i[ + id key value variable_type created_at updated_at + ] + end + + specify { expect(described_class.graphql_name).to eq('WorkspaceVariable') } + + specify { expect(described_class).to have_graphql_fields(fields) } + + specify { expect(described_class).to require_graphql_authorizations(:read_workspace_variable) } +end diff --git a/ee/spec/lib/remote_development/workspace_operations/create/workspace_variables_spec.rb b/ee/spec/lib/remote_development/workspace_operations/create/workspace_variables_spec.rb index 5cf2f3451c86c3de5675b2a8651fcd6cdb4b2cf6..23386173597e5a61eb07cf6192d2aaedb64711cf 100644 --- a/ee/spec/lib/remote_development/workspace_operations/create/workspace_variables_spec.rb +++ b/ee/spec/lib/remote_development/workspace_operations/create/workspace_variables_spec.rb @@ -142,12 +142,14 @@ { key: "VAR1", value: "value 1", + user_provided: true, variable_type: RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES[:environment], workspace_id: workspace_id }, { key: "/path/to/file", value: "value 2", + user_provided: true, variable_type: RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES[:file], workspace_id: workspace_id } diff --git a/ee/spec/policies/remote_development/workspace_variable_policy_spec.rb b/ee/spec/policies/remote_development/workspace_variable_policy_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..46ce96303bc9b2ca5882189f32b6bab1702c02eb --- /dev/null +++ b/ee/spec/policies/remote_development/workspace_variable_policy_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RemoteDevelopment::WorkspaceVariablePolicy, feature_category: :workspaces do + include AdminModeHelper + using RSpec::Parameterized::TableSyntax + + let_it_be(:agent_project_creator, refind: true) { create(:user) } + let_it_be(:agent_project, refind: true) { create(:project, creator: agent_project_creator) } + let_it_be(:agent, refind: true) do + create(:ee_cluster_agent, :with_existing_workspaces_agent_config, project: agent_project) + end + + let_it_be(:workspace_project_creator, refind: true) { create(:user) } + let_it_be(:workspace_project, refind: true) { create(:project, creator: workspace_project_creator) } + let_it_be(:workspace_owner, refind: true) { create(:user) } + let_it_be(:workspace, refind: true) do + create(:workspace, project: workspace_project, agent: agent, user: workspace_owner) + end + + let_it_be(:workspace_variable, refind: true) do + create(:workspace_variable, workspace: workspace) + end + + let_it_be(:admin_user, refind: true) { create(:admin) } + let_it_be(:non_admin_user, refind: true) { create(:user) } + # NOTE: The following need to be `let`, not `let_it_be`, because it uses a `let` declaration from the matrix + let(:user) { admin_mode ? admin_user : non_admin_user } + + let(:policy_class) { described_class } + + subject(:policy_instance) { policy_class.new(user, workspace_variable) } + + before do + stub_licensed_features(remote_development: licensed) + enable_admin_mode!(user) if admin_mode + workspace.update!(user: user) if workspace_owner + agent_project.add_role(user, role_on_agent_project) unless role_on_agent_project == :none + agent_project.reload + # noinspection RubyResolve - https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/tracked-jetbrains-issues/#ruby-31543 + workspace_project.add_role(user, role_on_workspace_project) unless role_on_workspace_project == :none + workspace_project.reload + user.reload + + debug = false # Set to true to enable debugging of policies, but change back to false before committing + debug_policies(user, workspace_variable, policy_class, ability) if debug + end + + shared_examples 'fixture sanity checks' do + # noinspection RubyResolve -- Rubymine is incorrectly resolving workspace_project as `QA::Resource::Project`. + it "has fixture sanity checks" do + expect(agent_project.creator_id).not_to eq(workspace_project.creator_id) + expect(agent_project.creator_id).not_to eq(user.id) + expect(workspace_project.creator_id).not_to eq(user.id) + expect(agent.created_by_user_id).not_to eq(workspace.user_id) + expect(workspace.user_id).not_to eq(user.id) unless workspace_owner + end + end + + # rubocop:disable Layout/LineLength -- TableSyntax should not be split across lines + where(:admin, :admin_mode, :licensed, :workspace_owner, :role_on_workspace_project, :role_on_agent_project, :allowed) do + # @formatter:off - Turn off RubyMine autoformatting + + # admin | # admin_mode | # licensed | workspace_owner | role_on_workspace_project | role_on_agent_project | allowed # check + true | true | false | false | :none | :none | false # admin_mode enabled but not licensed: not allowed + false | false | false | true | :developer | :none | false # Workspace owner and project developer but not licensed: not allowed + false | false | true | true | :guest | :none | false # Workspace owner but project guest: not allowed + false | false | false | false | :none | :maintainer | false # Cluster agent admin but not licensed: not allowed + false | false | true | false | :none | :developer | false # Not a cluster agent admin (must be maintainer): not allowed + true | false | true | false | :none | :none | false # admin but admin_mode not enabled and licensed: not allowed + true | true | true | false | :none | :none | true # admin_mode enabled and licensed: allowed + false | false | true | true | :developer | :none | true # Workspace owner and project developer: allowed + false | false | true | false | :none | :maintainer | true # Cluster agent admin: allowed + + # @formatter:on + end + # rubocop:enable Layout/LineLength + + with_them do + # NOTE: Currently :read_workspace and :update_workspace abilities have identical rules, so we can test them with + # the same table checks. If their behavior diverges in the future, we'll need to duplicate the table checks. + + describe "read_workspace_variable ability" do + let(:ability) { :read_workspace_variable } + + it_behaves_like 'fixture sanity checks' + + it { is_expected.to(allowed ? be_allowed(:read_workspace_variable) : be_disallowed(:read_workspace_variable)) } + end + end + + # NOTE: Leaving this method here for future use. You can also set GITLAB_DEBUG_POLICIES=1. For more details, see: + # https://docs.gitlab.com/ee/development/permissions/custom_roles.html#refactoring-abilities + # This may be generalized in the future for use across all policy specs + # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/463453 + def debug_policies(user, workspace, policy_class, ability) + puts "\n\nPolicy debug for #{policy_class} policy:\n" + puts "user: #{user.username} (id: #{user.id}, admin: #{user.admin?}, " \ + "admin_mode: #{user && Gitlab::Auth::CurrentUserMode.new(user).admin_mode?}" \ + ")\n" + + policy = policy_class.new(user, workspace) + puts "debugging :#{ability} ability:\n\n" + pp policy.debug(ability) + puts "\n\n" + end +end diff --git a/ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb b/ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb index b86b8d572868aacc83f3c875bc1157b592bb7982..0340378c23a1e822a5a5bb2abdb4be04a4c0fe7a 100644 --- a/ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb @@ -37,6 +37,20 @@ let(:desired_state) { RemoteDevelopment::WorkspaceOperations::States::RUNNING } + let(:mutation_expected_varaiables) do + [ + { key: 'VAR1', value: 'value 1', type: 'ENVIRONMENT', variable_type: 'ENVIRONMENT' }, + { key: 'VAR2', value: 'value 2', type: 'ENVIRONMENT', variable_type: 'ENVIRONMENT' } + ] + end + + let(:service_class_expected_variables) do + [ + { key: 'VAR1', value: 'value 1', type: 'ENVIRONMENT', variable_type: 0 }, + { key: 'VAR2', value: 'value 2', type: 'ENVIRONMENT', variable_type: 0 } + ] + end + let(:all_mutation_args) do { desired_state: desired_state, @@ -46,10 +60,7 @@ project_id: workspace_project.to_global_id.to_s, project_ref: 'main', devfile_path: '.devfile.yaml', - variables: [ - { key: 'VAR1', value: 'value 1', type: 'ENVIRONMENT' }, - { key: 'VAR2', value: 'value 2', type: 'ENVIRONMENT' } - ] + variables: mutation_expected_varaiables } end @@ -61,6 +72,7 @@ let(:expected_service_args) do params = all_mutation_args.except(:cluster_agent_id, :project_id) + params[:variables] = service_class_expected_variables params[:agent] = agent params[:user] = current_user params[:project] = workspace_project diff --git a/ee/spec/requests/api/graphql/remote_development/README.md b/ee/spec/requests/api/graphql/remote_development/README.md index b7e0d69d659c31fa69d031e9f37335f50ccf5e72..9fbc5db7f6bee089385452453c23d8456fac45ca 100644 --- a/ee/spec/requests/api/graphql/remote_development/README.md +++ b/ee/spec/requests/api/graphql/remote_development/README.md @@ -14,7 +14,7 @@ Here are the related spec folders for the fields (in alphabetical order by resol - GraphQL Field: `Query.workspaces` - Spec folder: `ee/spec/requests/api/graphql/remote_development/workspaces` - API docs: https://docs.gitlab.com/ee/api/graphql/reference/index.html#queryworkspaces - - Resolver source file for `tests.yml` and `verify-tff-mapping`: `ee/app/graphql/resolvers/remote_development/admin_workspaces_resolver.rb` + - Resolver source file for `tests.yml` and `verify-tff-mapping`: `ee/app/graphql/resolvers/remote_development/workspaces_admin_resolver.rb` - Notes: Only admins may use this field. - GraphQL Field: `Query.project.clusterAgent.remoteDevelopmentAgentConfig` @@ -51,6 +51,12 @@ Here are the related spec folders for the fields (in alphabetical order by resol - Resolver source file for `tests.yml` and `verify-tff-mapping`: `ee/app/graphql/resolvers/remote_development/namespace/workspaces_resolver.rb` - Notes: This is the same resolver used by `Query.currentUser.workspaces` +- GraphQL Field: `Query.workspace.workspaceVariables` + - Spec folder: `ee/spec/requests/api/graphql/remote_development/workspace_variables` + - API docs: https://docs.gitlab.com/ee/api/graphql/reference/index.html##workspacevariableconnection + - Resolver source file for `tests.yml` and `verify-tff-mapping`: `ee/app/graphql/resolvers/remote_development/namespace/workspaces_resolver.rb` + - Notes: This is the same resolver used by `Query.currentUser.workspaces` + - GraphQL Field: `Query.currentUser.workspaces` - Spec folder: `ee/spec/requests/api/graphql/remote_development/current_user/workspaces` - API docs: https://docs.gitlab.com/ee/api/graphql/reference/index.html#currentuserworkspaces @@ -75,6 +81,8 @@ Without this approach, achieving equivalent coverage across all of this same Gra If you add new spec files, you should update `tests.yml` and `scripts/verify-tff-mapping` accordingly. +Add entries for relevant types and resolvers in these files. + ## Why aren't all individual fields of graphql types tested in these specs? None of these other graphql API request specs test the actual fields because that’s not necessary. diff --git a/ee/spec/requests/api/graphql/remote_development/workspace_variables/shared.rb b/ee/spec/requests/api/graphql/remote_development/workspace_variables/shared.rb new file mode 100644 index 0000000000000000000000000000000000000000..0c8b04b69346d99f088ec62514a23ad724a4c3af --- /dev/null +++ b/ee/spec/requests/api/graphql/remote_development/workspace_variables/shared.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require_relative '../shared' + +RSpec.shared_context 'with multiple workspace_variables created' do + include_context 'with unauthorized workspace created' + + let_it_be(:workspace, reload: true) { create(:workspace, name: 'matching-workspace') } + + let(:id) { workspace.to_global_id.to_s } + let(:args) { { id: id } } + + let_it_be(:workspace_a_internal_variable) do + create(:workspace_variable, + workspace_id: workspace.id, + key: 'GIT_CONFIG_COUNT', + value: 'internal_var', + variable_type: 0, + user_provided: false + ) + end + + let_it_be(:workspace_user_env_variable) do + create(:workspace_variable, + workspace_id: workspace.id, + key: 'GIT_CONFIG_KEY_1', + value: 'user_var_1', + variable_type: 0, + user_provided: true + ) + end + + let_it_be(:workspace_user_file_variable) do + create(:workspace_variable, + workspace_id: workspace.id, + key: 'CONFIG_FILE', + value: 'user_var_1', + variable_type: 1, + user_provided: true + ) + end + + let_it_be(:unauthorized_workspace_variable) do + create(:workspace_variable, + workspace_id: unauthorized_workspace.id, + key: 'OTHER_VAR', + value: 'other_var', + variable_type: 0, + user_provided: true + ) + end +end + +RSpec.shared_context 'for a Query.workspace.workspaceVariables query' do + include GraphqlHelpers + + include_context 'with multiple workspace_variables created' + include_context "with authorized user as developer on workspace's project" + + let(:query) do + fields = all_graphql_fields_for('WorkspaceVariable') + + graphql_query_for( + :workspace, + args, + query_nodes(:workspace_variables, fields) + ) + end + + subject(:actual_workspace_variables) { graphql_dig_at(graphql_data, :workspace, :workspace_variables, :nodes) } +end + +RSpec.shared_examples 'query returns an array containing all non-internal variables associated with a workspace' do + before do + post_graphql(query, current_user: current_user) + end + + it 'includes only the expected workspace_variables', :unlimited_max_formatted_output_length do + expect(workspace_variable_keys).not_to include(unauthorized_workspace_variable.key) + expect(workspace_variable_keys).not_to include(workspace_user_file_variable.key) + expect(workspace_variable_keys).not_to include(workspace_a_internal_variable.key) + + expect(workspace_variable_keys).to include(workspace_user_env_variable.key) + expect(workspace_variable_values).to include(workspace_user_env_variable.value) + end + + it 'includes only the correct type of workspace_variables' do + expect(workspace_variable_types).to include( + ::RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES.key(0).to_s.upcase + ) + expect(workspace_variable_types).not_to include( + ::RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES.key(1).to_s.upcase + ) + end +end + +RSpec.shared_examples 'multiple workspace_variables query' do |authorized_user_is_admin: false| + include_context 'in licensed environment' + + let(:workspace_variable_keys) { subject.pluck("key") } + let(:workspace_variable_values) { subject.pluck("value") } + let(:workspace_variable_types) { subject.pluck("variableType") } + + context 'when user is authorized' do + include_context 'with authorized user as current user' + + it_behaves_like 'query is a working graphql query' + it_behaves_like 'query returns an array containing all non-internal variables associated with a workspace' + + unless authorized_user_is_admin + context 'when the user requests a workspace that they are not authorized for' do + let(:id) { global_id_of(unauthorized_workspace) } + + it_behaves_like 'query is a working graphql query' + it_behaves_like 'query returns blank' + end + end + end + + context 'when user is not authorized' do + include_context 'with unauthorized user as current user' + + it_behaves_like 'query is a working graphql query' + it_behaves_like 'query returns blank' + end + + it_behaves_like 'query in unlicensed environment' +end diff --git a/ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb b/ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..36565bb7702bb8e74fd7cf61d675dad6c19ae00f --- /dev/null +++ b/ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative './shared' + +RSpec.describe 'Query.workspace.workspaceVariables (with no arguments)', feature_category: :workspaces do + include_context 'for a Query.workspace.workspaceVariables query' + + context 'with non-admin user' do + let_it_be(:authorized_user) { workspace.user } + let_it_be(:unauthorized_user) { create(:user) } + + it_behaves_like 'multiple workspace_variables query' + end + + context 'with admin user' do + let_it_be(:authorized_user) { create(:admin) } + let_it_be(:unauthorized_user) { create(:user) } + + it_behaves_like 'multiple workspace_variables query', authorized_user_is_admin: true + end +end diff --git a/ee/spec/requests/remote_development/integration_spec.rb b/ee/spec/requests/remote_development/integration_spec.rb index 184059b152b59441970248f5461c82f9d0f22b08..4a2fdb077c15806dc3f899a39b5f2c372647ad2e 100644 --- a/ee/spec/requests/remote_development/integration_spec.rb +++ b/ee/spec/requests/remote_development/integration_spec.rb @@ -87,7 +87,7 @@ ] end - let(:expected_static_variables) do + let(:expected_internal_variables) do # rubocop:disable Layout/LineLength -- keep them on one line for easier readability and editability [ { key: "GIT_CONFIG_COUNT", type: :environment, value: "3" }, @@ -237,15 +237,20 @@ def do_create_workspace expect(actual_processed_devfile.fetch(:components)).to eq(expected_processed_devfile.fetch(:components)) expect(actual_processed_devfile).to eq(expected_processed_devfile) - all_expected_vars = (expected_static_variables + user_provided_variables).sort_by { |v| v[:key] } + all_expected_vars = (expected_internal_variables + user_provided_variables).sort_by { |v| v[:key] } # NOTE: We convert the actual records into hashes and sort them as a hash rather than ordering in # ActiveRecord, to account for platform- or db-specific sorting differences. types = RemoteDevelopment::Enums::Workspace::WORKSPACE_VARIABLE_TYPES - all_actual_vars = - RemoteDevelopment::WorkspaceVariable - .where(workspace: workspace) - .map { |v| { key: v.key, type: types.invert[v.variable_type], value: v.value } } - .sort_by { |v| v[:key] } + all_actual_vars = RemoteDevelopment::WorkspaceVariable.where(workspace: workspace) + + actual_user_provided_vars = all_actual_vars.select(&:user_provided) + + all_actual_vars = all_actual_vars.map { |v| { key: v.key, type: types.invert[v.variable_type], value: v.value } } + .sort_by { |v| v[:key] } + + # Check that user provided variables had their flag set correctly. + expect(actual_user_provided_vars.count).to eq(user_provided_variables.count) + expect(actual_user_provided_vars[0][:key]).to eq(user_provided_variables[0][:key]) # Check just keys first, to get an easy failure message if a new key has been added expect(all_actual_vars.pluck(:key)).to match_array(all_expected_vars.pluck(:key)) @@ -255,7 +260,7 @@ def do_create_workspace actual_without_regexes = all_actual_vars.reject { |v| v[:key] == "gl_token" } expect(expected_without_regexes).to match(actual_without_regexes) - expected_gl_token_value = expected_static_variables.find { |var| var[:key] == "gl_token" }[:value] + expected_gl_token_value = expected_internal_variables.find { |var| var[:key] == "gl_token" }[:value] actual_gl_token_value = all_actual_vars.find { |var| var[:key] == "gl_token" }[:value] expect(actual_gl_token_value).to match(expected_gl_token_value) diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping index 0bd94ee6f6f642cf2739cc7d65268ffe7582d880..689f9ab1687621be4e0943aa862ed853170cac23 100755 --- a/scripts/verify-tff-mapping +++ b/scripts/verify-tff-mapping @@ -472,18 +472,6 @@ tests = [ ## END Remote development GraphQL mutations ## BEGIN Remote development GraphQL resolvers (in alphabetical order by resolver source file path) - { - explanation: 'Map Remote Development GraphQL query root admin_workspaces_resolver.rb to request specs', - changed_file: 'ee/app/graphql/resolvers/remote_development/admin_workspaces_resolver.rb', - expected: %w[ - ee/spec/requests/api/graphql/remote_development/workspace/with_id_arg_spec.rb - ee/spec/requests/api/graphql/remote_development/workspaces/with_actual_states_arg_spec.rb - ee/spec/requests/api/graphql/remote_development/workspaces/with_agent_ids_arg_spec.rb - ee/spec/requests/api/graphql/remote_development/workspaces/with_ids_arg_spec.rb - ee/spec/requests/api/graphql/remote_development/workspaces/with_no_args_spec.rb - ee/spec/requests/api/graphql/remote_development/workspaces/with_project_ids_arg_spec.rb - ] - }, { explanation: 'Map Remote Development GraphQL cluster_agent/remote_development_agent_config_resolver.rb to request specs', @@ -502,6 +490,7 @@ tests = [ ee/spec/requests/api/graphql/remote_development/cluster_agent/workspaces/with_ids_arg_spec.rb ee/spec/requests/api/graphql/remote_development/cluster_agent/workspaces/with_no_args_spec.rb ee/spec/requests/api/graphql/remote_development/cluster_agent/workspaces/with_project_ids_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb ] }, { @@ -526,6 +515,19 @@ tests = [ ] # rubocop:enable Layout/LineLength }, + { + explanation: 'Map Remote Development GraphQL query root workspaces_admin_resolver.rb to request specs', + changed_file: 'ee/app/graphql/resolvers/remote_development/workspaces_admin_resolver.rb', + expected: %w[ + ee/spec/requests/api/graphql/remote_development/workspace/with_id_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspaces/with_actual_states_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspaces/with_agent_ids_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspaces/with_ids_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspaces/with_no_args_spec.rb + ee/spec/requests/api/graphql/remote_development/workspaces/with_project_ids_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb + ] + }, { explanation: 'Map Remote Development GraphQL workspaces_resolver.rb to request specs', changed_file: 'ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb', @@ -536,9 +538,12 @@ tests = [ ee/spec/requests/api/graphql/remote_development/current_user/workspaces/with_ids_arg_spec.rb ee/spec/requests/api/graphql/remote_development/current_user/workspaces/with_no_args_spec.rb ee/spec/requests/api/graphql/remote_development/current_user/workspaces/with_project_ids_arg_spec.rb + ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb ] }, + ## END Remote development GraphQL resolvers + ## BEGIN Remote development GraphQL types { explanation: 'Map Remote Development GraphQL query root workspace type resolver to request specs', changed_file: 'ee/app/graphql/types/remote_development/workspace_type.rb', @@ -547,7 +552,37 @@ tests = [ ee/spec/requests/api/graphql/remote_development/workspace/with_id_arg_spec.rb ] }, - ## END Remote development GraphQL resolvers + { + explanation: 'Map Remote Development GraphQL workspace_variable_type.rb to request specs', + changed_file: 'ee/app/graphql/types/remote_development/workspace_variable_type.rb', + expected: %w[ + ee/spec/graphql/types/remote_development/workspace_variable_type_spec.rb + ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb + ] + }, + { + explanation: 'Map Remote Development GraphQL workspace_variable_input_type_enum.rb to request specs', + changed_file: 'ee/app/graphql/types/remote_development/workspace_variable_input.rb', + expected: %w[ + ee/spec/graphql/types/remote_development/workspace_variable_input_spec.rb + ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb + ] + }, + { + explanation: 'Map Remote Development GraphQL workspace_variable_type_enum.rb to request specs', + changed_file: 'ee/app/graphql/types/remote_development/workspace_variable_type_enum.rb', + expected: %w[ + ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb + ] + }, + { + explanation: 'Map Remote Development GraphQL workspace_variable_input_type_enum.rb to request specs', + changed_file: 'ee/app/graphql/types/remote_development/workspace_variable_input_type_enum.rb', + expected: %w[ + ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb + ] + }, + ## END Remote development GraphQL types { explanation: 'https://gitlab.com/gitlab-org/gitlab/-/issues/466068#note_1987834618', diff --git a/spec/migrations/20250114194610_backfill_user_provided_workspace_variables_spec.rb b/spec/migrations/20250114194610_backfill_user_provided_workspace_variables_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eaa67992b0ddcb28d88c72a69a1494654e9706e1 --- /dev/null +++ b/spec/migrations/20250114194610_backfill_user_provided_workspace_variables_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillUserProvidedWorkspaceVariables, feature_category: :workspaces do + let(:user) { table(:users).create!(name: 'test-user', email: 'test@example.com', projects_limit: 5) } + let(:organization) { table(:organizations).create!(name: 'test-org', path: 'default') } + let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path', organization_id: organization.id) } + let(:workspace_variables) { table(:workspace_variables) } + + let(:project) do + table(:projects).create!(namespace_id: namespace.id, project_namespace_id: namespace.id, + organization_id: organization.id) + end + + let!(:personal_access_token) do + table(:personal_access_tokens).create!( + user_id: user.id, + name: 'token_name', + organization_id: organization.id, + expires_at: Time.now + ) + end + + let(:cluster_agent) { table(:cluster_agents).create!(name: 'remotedev', project_id: project.id) } + + let!(:agent) do + table(:workspaces_agent_configs).create!( + cluster_agent_id: cluster_agent.id, + enabled: true, + dns_zone: 'test.workspace.me', + project_id: project.id + ) + end + + let!(:agent_config_version) do + table(:workspaces_agent_config_versions).create!( + project_id: project.id, + item_id: agent.id, + item_type: 'RemoteDevelopment::WorkspacesAgentConfig', + event: 'create' + ) + end + + let!(:workspace) do + table(:workspaces).create!( + user_id: user.id, + project_id: project.id, + cluster_agent_id: cluster_agent.id, + desired_state_updated_at: Time.now, + responded_to_agent_at: Time.now, + name: 'workspace-1', + namespace: 'workspace_1_namespace', + desired_state: 'Running', + actual_state: 'Running', + project_ref: 'devfile-ref', + devfile_path: 'devfile-path', + devfile: 'devfile', + processed_devfile: 'processed_dev_file', + url: 'workspace-url', + deployment_resource_version: 'v1', + personal_access_token_id: personal_access_token.id, + max_hours_before_termination: 5760, + workspaces_agent_config_version: agent_config_version.id, + desired_config_generator_version: 3 + ) + end + + let!(:workspace_static_file_variable) do + workspace_variables.create!( + workspace_id: workspace.id, + project_id: project.id, + key: 'gl_token', + variable_type: 1, + encrypted_value: 'encrypted_value', + encrypted_value_iv: 'encrypted_value_iv' + ) + end + + let!(:workspace_static_env_variable) do + workspace_variables.create!( + workspace_id: workspace.id, + project_id: project.id, + key: 'GIT_CONFIG_COUNT', + variable_type: 0, + encrypted_value: 'encrypted_value', + encrypted_value_iv: 'encrypted_value_iv' + ) + end + + let!(:workspace_user_provided_variable) do + workspace_variables.create!( + workspace_id: workspace.id, + project_id: project.id, + key: 'variable_key', + variable_type: 0, + encrypted_value: 'encrypted_value', + encrypted_value_iv: 'encrypted_value_iv' + ) + end + + it 'sets user_provided to true for all non-internal environment variables in the table' do + reversible_migration do |migration| + migration.before -> { + expect(workspace_variables.pluck(:user_provided)).to all(be false) + } + + migration.after -> { + user_provided_variable = workspace_variables.where(variable_type: described_class::VARIABLE_ENV_TYPE) + .where.not(key: described_class::WORKSPACE_INTERNAL_VARIABLES).first + + expect(workspace_static_file_variable.user_provided).to be(false) + expect(workspace_static_env_variable.user_provided).to be(false) + expect(user_provided_variable.user_provided).to be(true) + } + end + end +end diff --git a/tests.yml b/tests.yml index 6c4a2fa01686e3e9f50e73d60f282454d645356b..0c9b2463dc0c7a997d20a8fd1e005816dffdad80 100644 --- a/tests.yml +++ b/tests.yml @@ -124,10 +124,11 @@ mapping: test: 'spec/mailers/previews_spec.rb' ## BEGIN Remote development GraphQL resolvers (in alphabetical order by resolver source file path) - - source: 'ee/app/graphql/resolvers/remote_development/admin_workspaces_resolver\.rb' + - source: 'ee/app/graphql/resolvers/remote_development/workspaces_admin_resolver\.rb' test: - 'ee/spec/requests/api/graphql/remote_development/workspace/*_spec.rb' - 'ee/spec/requests/api/graphql/remote_development/workspaces/*_spec.rb' + - 'ee/spec/requests/api/graphql/remote_development/workspace_variables/*_spec.rb' - source: 'ee/app/graphql/resolvers/remote_development/cluster_agent/remote_development_agent_config_resolver\.rb' test: @@ -140,10 +141,12 @@ mapping: - source: 'ee/app/graphql/resolvers/remote_development/cluster_agent/workspaces_resolver\.rb' test: - 'ee/spec/requests/api/graphql/remote_development/cluster_agent/workspaces/*_spec.rb' + - 'ee/spec/requests/api/graphql/remote_development/workspace_variables/*_spec.rb' - source: 'ee/app/graphql/resolvers/remote_development/workspaces_resolver\.rb' test: - 'ee/spec/requests/api/graphql/remote_development/current_user/workspaces/*_spec.rb' + - 'ee/spec/requests/api/graphql/remote_development/workspace_variables/*_spec.rb' - source: 'ee/app/graphql/resolvers/remote_development/namespace/cluster_agents_resolver\.rb' test: @@ -154,13 +157,35 @@ mapping: test: - 'ee/spec/requests/api/graphql/remote_development/current_user/workspaces/*_spec.rb' - 'ee/spec/requests/api/graphql/remote_development/workspace/*_spec.rb' + - 'ee/spec/requests/api/graphql/remote_development/workspace_variables/*_spec.rb' ## END Remote development GraphQL resolvers + ## BEGIN Remote development GraphQL types + - source: 'ee/app/graphql/types/remote_development/workspace_type\.rb' test: - 'ee/spec/requests/api/graphql/remote_development/workspace/with_id_arg_spec.rb' + - source: 'ee/app/graphql/types/remote_development/workspace_variable_input\.rb' + test: + - 'ee/spec/graphql/types/remote_development/workspace_variable_input_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb' + + - source: 'ee/app/graphql/types/remote_development/workspace_variable_type\.rb' + test: + - 'ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb' + + - source: 'ee/app/graphql/types/remote_development/workspace_variable_type_enum\.rb' + test: + - 'ee/spec/requests/api/graphql/remote_development/workspace_variables/with_no_args_spec.rb' + + - source: 'ee/app/graphql/types/remote_development/workspace_variable_input_type_enum\.rb' + test: + - 'ee/spec/requests/api/graphql/mutations/remote_development/workspace_operations/create_spec.rb' + + ## END Remote development GraphQL types + # Usage metric schema changes should trigger validations for all metrics and tooling - source: 'config/metrics/schema/.*\.json' test: