From 23f3a4b539c318409c14b83a52a4066cebf252f9 Mon Sep 17 00:00:00 2001 From: Daniyal Arshad Date: Fri, 29 Nov 2024 13:36:12 -0500 Subject: [PATCH 1/6] Add workspace_ports table & ports creator logic --- .../javascripts/graphql_shared/constants.js | 1 + db/docs/workspace_ports.yml | 15 ++ .../20241127191713_create_workspace_ports.rb | 15 ++ db/schema_migrations/20241127191713 | 1 + db/structure.sql | 30 +++ doc/api/graphql/reference/_index.md | 65 ++++++ .../workspace_port_state_indicator.vue | 54 +++++ .../common/components/workspace_ports.vue | 191 ++++++++++++++++++ .../common/components/workspace_tab.vue | 3 + .../workspaces/common/constants.js | 2 + .../fragments/workspace_item.fragment.graphql | 5 + .../workspace_port_item.fragment.graphql | 7 + .../workspace_port_create.mutation.graphql | 10 + .../workspace_port_update.mutation.graphql | 9 + ee/app/graphql/ee/types/mutation_type.rb | 2 + .../workspace_ports_operations/create.rb | 63 ++++++ .../workspace_ports_operations/update.rb | 57 ++++++ .../remote_development/workspace_port_type.rb | 30 +++ .../remote_development/workspace_type.rb | 5 + ee/app/models/remote_development/workspace.rb | 1 + .../remote_development/workspace_port.rb | 16 ++ .../remote_development/workspace_policy.rb | 1 + .../workspace_port_policy.rb | 8 + ee/lib/remote_development/messages.rb | 4 + .../workspace_operations/create/creator.rb | 3 +- .../create/workspace_devfile_ports_creator.rb | 56 +++++ .../workspace_operations/port_visibility.rb | 13 ++ .../output/desired_config_generator.rb | 51 ++++- .../workspace_ports_operations/create/main.rb | 30 +++ .../create/port_creator.rb | 39 ++++ .../workspace_ports_operations/update/main.rb | 30 +++ .../update/updater.rb | 23 +++ .../remote_development/workspace_ports.rb | 9 + locale/gitlab.pot | 28 +++ 34 files changed, 875 insertions(+), 2 deletions(-) create mode 100644 db/docs/workspace_ports.yml create mode 100644 db/migrate/20241127191713_create_workspace_ports.rb create mode 100644 db/schema_migrations/20241127191713 create mode 100644 ee/app/assets/javascripts/workspaces/common/components/workspace_port_state_indicator.vue create mode 100644 ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue create mode 100644 ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_port_item.fragment.graphql create mode 100644 ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_create.mutation.graphql create mode 100644 ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_update.mutation.graphql create mode 100644 ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb create mode 100644 ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb create mode 100644 ee/app/graphql/types/remote_development/workspace_port_type.rb create mode 100644 ee/app/models/remote_development/workspace_port.rb create mode 100644 ee/app/policies/remote_development/workspace_port_policy.rb create mode 100644 ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb create mode 100644 ee/lib/remote_development/workspace_operations/port_visibility.rb create mode 100644 ee/lib/remote_development/workspace_ports_operations/create/main.rb create mode 100644 ee/lib/remote_development/workspace_ports_operations/create/port_creator.rb create mode 100644 ee/lib/remote_development/workspace_ports_operations/update/main.rb create mode 100644 ee/lib/remote_development/workspace_ports_operations/update/updater.rb create mode 100644 ee/spec/factories/remote_development/workspace_ports.rb diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js index 71ad03b82147a3..ef085ae3020e35 100644 --- a/app/assets/javascripts/graphql_shared/constants.js +++ b/app/assets/javascripts/graphql_shared/constants.js @@ -40,6 +40,7 @@ export const TYPENAME_WORK_ITEM_RELATED_BRANCH = 'WorkItemRelatedBranch'; export const TYPE_ORGANIZATION = 'Organizations::Organization'; export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply'; export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace'; +export const TYPE_WORKSPACE_PORT = 'RemoteDevelopment::WorkspacePort'; export const TYPE_COMPLIANCE_FRAMEWORK = 'ComplianceManagement::Framework'; export const TYPENAME_CUSTOM_FIELD = 'Issuables::CustomField'; export const TYPENAME_CUSTOM_FIELD_SELECT_OPTION = 'Issuables::CustomFieldSelectOption'; diff --git a/db/docs/workspace_ports.yml b/db/docs/workspace_ports.yml new file mode 100644 index 00000000000000..c401dab2ff45c1 --- /dev/null +++ b/db/docs/workspace_ports.yml @@ -0,0 +1,15 @@ +--- +table_name: workspace_ports +classes: +- RemoteDevelopment::WorkspacePort +feature_categories: +- workspaces +description: Remote Development Workspace ports +introduced_by_url: +milestone: '17.7' +gitlab_schema: gitlab_main_cell +allow_cross_foreign_keys: +- gitlab_main_clusterwide +sharding_key: + project_id: projects +table_size: small diff --git a/db/migrate/20241127191713_create_workspace_ports.rb b/db/migrate/20241127191713_create_workspace_ports.rb new file mode 100644 index 00000000000000..1093fc5135cf5b --- /dev/null +++ b/db/migrate/20241127191713_create_workspace_ports.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateWorkspacePorts < Gitlab::Database::Migration[2.2] + milestone '17.7' + + def change + create_table :workspace_ports do |t| + t.references :workspace, null: false, foreign_key: { on_delete: :cascade } + t.boolean :user_provided, null: false, default: false + t.integer :port_number, null: false + t.text :visibility, null: false, default: 'private', limit: 256 + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/schema_migrations/20241127191713 b/db/schema_migrations/20241127191713 new file mode 100644 index 00000000000000..59bbf345a39605 --- /dev/null +++ b/db/schema_migrations/20241127191713 @@ -0,0 +1 @@ +77b33b9eb31a655eafb2d299ce17b60cd4bbe461e90cfb1ef0166faad825ccf0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 623f76b825fe9a..32742f6279513f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25836,6 +25836,26 @@ CREATE SEQUENCE work_item_widget_definitions_id_seq ALTER SEQUENCE work_item_widget_definitions_id_seq OWNED BY work_item_widget_definitions.id; +CREATE TABLE workspace_ports ( + id bigint NOT NULL, + workspace_id bigint NOT NULL, + user_provided boolean DEFAULT false NOT NULL, + port_number integer NOT NULL, + visibility text DEFAULT 'private'::text NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_7b90c8d8c2 CHECK ((char_length(visibility) <= 256)) +); + +CREATE SEQUENCE workspace_ports_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE workspace_ports_id_seq OWNED BY workspace_ports.id; + CREATE TABLE workspace_variables ( id bigint NOT NULL, workspace_id bigint NOT NULL, @@ -28052,6 +28072,8 @@ ALTER TABLE ONLY work_item_type_user_preferences ALTER COLUMN id SET DEFAULT nex ALTER TABLE ONLY work_item_widget_definitions ALTER COLUMN id SET DEFAULT nextval('work_item_widget_definitions_id_seq'::regclass); +ALTER TABLE ONLY workspace_ports ALTER COLUMN id SET DEFAULT nextval('workspace_ports_id_seq'::regclass); + ALTER TABLE ONLY workspace_variables ALTER COLUMN id SET DEFAULT nextval('workspace_variables_id_seq'::regclass); ALTER TABLE ONLY workspaces ALTER COLUMN id SET DEFAULT nextval('workspaces_id_seq'::regclass); @@ -31277,6 +31299,9 @@ ALTER TABLE ONLY work_item_weights_sources ALTER TABLE ONLY work_item_widget_definitions ADD CONSTRAINT work_item_widget_definitions_pkey PRIMARY KEY (id); +ALTER TABLE ONLY workspace_ports + ADD CONSTRAINT workspace_ports_pkey PRIMARY KEY (id); + ALTER TABLE ONLY workspace_variables ADD CONSTRAINT workspace_variables_pkey PRIMARY KEY (id); @@ -38067,6 +38092,8 @@ CREATE UNIQUE INDEX index_work_item_widget_definitions_on_type_id_and_name ON wo CREATE INDEX index_work_item_widget_definitions_on_work_item_type_id ON work_item_widget_definitions USING btree (work_item_type_id); +CREATE INDEX index_workspace_ports_on_workspace_id ON workspace_ports USING btree (workspace_id); + CREATE INDEX index_workspace_variables_on_project_id ON workspace_variables USING btree (project_id); CREATE INDEX index_workspace_variables_on_workspace_id ON workspace_variables USING btree (workspace_id); @@ -45680,6 +45707,9 @@ ALTER TABLE ONLY approval_policy_rules ALTER TABLE ONLY work_item_select_field_values ADD CONSTRAINT fk_rails_e3ecc2c14e FOREIGN KEY (custom_field_id) REFERENCES custom_fields(id) ON DELETE CASCADE; +ALTER TABLE ONLY workspace_ports + ADD CONSTRAINT fk_rails_e4267cb1f0 FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY clusters_integration_prometheus ADD CONSTRAINT fk_rails_e44472034c FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4909fe635713f7..af127473007fd9 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -13026,6 +13026,47 @@ Input type: `WorkspaceCreateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `workspace` | [`Workspace`](#workspace) | Created workspace. | +### `Mutation.workspacePortCreate` + +Input type: `WorkspacePortCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `portNumber` | [`Int!`](#int) | Number of the created port. | +| `visibility` | [`String!`](#string) | Visibility of the created port. | +| `workspaceId` | [`RemoteDevelopmentWorkspaceID!`](#remotedevelopmentworkspaceid) | GlobalID of the workspace. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `workspacePort` | [`WorkspacePort`](#workspaceport) | Created workspace port. | + +### `Mutation.workspacePortUpdate` + +Input type: `WorkspacePortUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `id` | [`RemoteDevelopmentWorkspacePortID!`](#remotedevelopmentworkspaceportid) | GlobalID of the workspace port. | +| `visibility` | [`String!`](#string) | Visibility of the created port. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `workspacePort` | [`WorkspacePort`](#workspaceport) | Updated workspace port. | + ### `Mutation.workspaceUpdate` Input type: `WorkspaceUpdateInput` @@ -42057,6 +42098,24 @@ Resource specifications of the workspace container. | ---- | ---- | ----------- | | `limits` | [`ResourceQuotas`](#resourcequotas) | Limits for the requested container resources of a workspace. | | `requests` | [`ResourceQuotas`](#resourcequotas) | Requested resources for the container of a workspace. | +| `workspacePorts` | [`[WorkspacePort!]`](#workspaceport) | Ports exposed on 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. | + +### `WorkspacePort` + +Represents a remote development workspace port. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `createdAt` | [`Time!`](#time) | Timestamp of when the workspace was created. | +| `id` | [`RemoteDevelopmentWorkspacePortID!`](#remotedevelopmentworkspaceportid) | Global ID of the workspace port. | +| `portNumber` | [`Int!`](#int) | Number of the workspace port. | +| `updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable workspace property. | +| `visibility` | [`String!`](#string) | Visibility status of the workspace port. | +| `workspaceId` | [`ID!`](#id) | ID of the workspace to which the port belongs. | ### `WorkspaceVariable` @@ -47084,6 +47143,12 @@ A `RemoteDevelopmentWorkspaceID` is a global ID. It is encoded as a string. An example `RemoteDevelopmentWorkspaceID` is: `"gid://gitlab/RemoteDevelopment::Workspace/1"`. +### `RemoteDevelopmentWorkspacePortID` + +A `RemoteDevelopmentWorkspacePortID` is a global ID. It is encoded as a string. + +An example `RemoteDevelopmentWorkspacePortID` is: `"gid://gitlab/RemoteDevelopment::WorkspacePort/1"`. + ### `RemoteDevelopmentWorkspaceVariableID` A `RemoteDevelopmentWorkspaceVariableID` is a global ID. It is encoded as a string. diff --git a/ee/app/assets/javascripts/workspaces/common/components/workspace_port_state_indicator.vue b/ee/app/assets/javascripts/workspaces/common/components/workspace_port_state_indicator.vue new file mode 100644 index 00000000000000..ca7a76cc428c43 --- /dev/null +++ b/ee/app/assets/javascripts/workspaces/common/components/workspace_port_state_indicator.vue @@ -0,0 +1,54 @@ + + diff --git a/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue b/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue new file mode 100644 index 00000000000000..42caa094b5e59c --- /dev/null +++ b/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue @@ -0,0 +1,191 @@ + + + diff --git a/ee/app/assets/javascripts/workspaces/common/components/workspace_tab.vue b/ee/app/assets/javascripts/workspaces/common/components/workspace_tab.vue index 4c9c8c3af6bdbb..9df2c138fbe2dd 100644 --- a/ee/app/assets/javascripts/workspaces/common/components/workspace_tab.vue +++ b/ee/app/assets/javascripts/workspaces/common/components/workspace_tab.vue @@ -5,6 +5,7 @@ import { WORKSPACES_LIST_PAGE_SIZE, I18N_LOADING_WORKSPACES_FAILED } from '../co import WorkspacesTable from './workspaces_list/workspaces_table.vue'; import WorkspaceEmptyState from './workspaces_list/empty_state.vue'; import WorkspacesListPagination from './workspaces_list/workspaces_list_pagination.vue'; +import WorkspacePorts from './workspace_ports.vue'; export const i18n = { loadingWorkspacesFailed: I18N_LOADING_WORKSPACES_FAILED, @@ -39,6 +40,7 @@ export default { WorkspacesTable, WorkspaceEmptyState, WorkspacesListPagination, + WorkspacePorts, }, props: { tabName: { @@ -108,6 +110,7 @@ export default { :page-size="$options.WORKSPACES_LIST_PAGE_SIZE" @input="onPaginationInput" /> + diff --git a/ee/app/assets/javascripts/workspaces/common/constants.js b/ee/app/assets/javascripts/workspaces/common/constants.js index 02e147c1937de9..a070a10b549c36 100644 --- a/ee/app/assets/javascripts/workspaces/common/constants.js +++ b/ee/app/assets/javascripts/workspaces/common/constants.js @@ -10,6 +10,8 @@ export const WORKSPACE_STATES = { stopped: 'Stopped', terminating: 'Terminating', terminated: 'Terminated', + portOpened: 'private', + portClosed: 'closed', failed: 'Failed', error: 'Error', unknown: 'Unknown', diff --git a/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_item.fragment.graphql b/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_item.fragment.graphql index 7fa49723f9fa20..433aa4a781e8bd 100644 --- a/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_item.fragment.graphql +++ b/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_item.fragment.graphql @@ -10,4 +10,9 @@ fragment WorkspaceItem on Workspace { devfilePath devfileWebUrl createdAt + workspacePorts { + id + portNumber + visibility + } } diff --git a/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_port_item.fragment.graphql b/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_port_item.fragment.graphql new file mode 100644 index 00000000000000..ceeb361ad177d5 --- /dev/null +++ b/ee/app/assets/javascripts/workspaces/common/graphql/fragments/workspace_port_item.fragment.graphql @@ -0,0 +1,7 @@ +fragment WorkspacePortItem on WorkspacePort { + id + workspaceId + portNumber + visibility + createdAt +} diff --git a/ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_create.mutation.graphql b/ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_create.mutation.graphql new file mode 100644 index 00000000000000..4aa240afb53c81 --- /dev/null +++ b/ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_create.mutation.graphql @@ -0,0 +1,10 @@ +mutation workspacePortCreate($input: WorkspacePortCreateInput!) { + workspacePortCreate(input: $input) { + workspacePort { + id + portNumber + visibility + } + errors + } +} diff --git a/ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_update.mutation.graphql b/ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_update.mutation.graphql new file mode 100644 index 00000000000000..2e9f737dfefe75 --- /dev/null +++ b/ee/app/assets/javascripts/workspaces/common/graphql/mutations/workspace_port_update.mutation.graphql @@ -0,0 +1,9 @@ +mutation workspacePortUpdagte($input: WorkspacePortUpdateInput!) { + workspacePortUpdate(input: $input) { + workspacePort { + id + visibility + } + errors + } +} diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 6bd39c92e1e8c2..a5562445017657 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -168,6 +168,8 @@ def self.authorization_scopes mount_mutation ::Mutations::Ci::Runners::ExportUsage mount_mutation ::Mutations::RemoteDevelopment::WorkspaceOperations::Create mount_mutation ::Mutations::RemoteDevelopment::WorkspaceOperations::Update + mount_mutation ::Mutations::RemoteDevelopment::WorkspacePortsOperations::Create + mount_mutation ::Mutations::RemoteDevelopment::WorkspacePortsOperations::Update mount_mutation ::Mutations::RemoteDevelopment::NamespaceClusterAgentMappingOperations::Create mount_mutation ::Mutations::RemoteDevelopment::NamespaceClusterAgentMappingOperations::Delete mount_mutation ::Mutations::RemoteDevelopment::OrganizationClusterAgentMappingOperations::Create, experiment: { diff --git a/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb new file mode 100644 index 00000000000000..aa5b870be64153 --- /dev/null +++ b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Mutations + module RemoteDevelopment + module WorkspacePortsOperations + class Create < BaseMutation + graphql_name 'WorkspacePortCreate' + + include Gitlab::Utils::UsageData + + authorize :create_workspace_port + + field :workspace_port, + Types::RemoteDevelopment::WorkspacePortType, + null: true, + description: 'Created workspace port.' + + argument :workspace_id, + ::Types::GlobalIDType[::RemoteDevelopment::Workspace], + required: true, + description: 'GlobalID of the workspace.' + + argument :port_number, + GraphQL::Types::Int, + required: true, + description: 'Number of the created port.' + + argument :visibility, + GraphQL::Types::String, + required: true, + description: 'Visibility of the created port.' + + # @param [Hash] args + def resolve(args) + unless License.feature_available?(:remote_development) + raise_resource_not_available_error!("'remote_development' licensed feature is not available") + end + + workspace_id = args.delete(:workspace_id) + + workspace = authorized_find!(id: workspace_id) + + domain_main_class_args = { + workspace: workspace, + params: args + } + + response = ::RemoteDevelopment::CommonService.execute( + domain_main_class: ::RemoteDevelopment::WorkspacePortsOperations::Create::Main, + domain_main_class_args: domain_main_class_args + ) + + response_object = response.success? ? response.payload[:workspace_port] : nil + + { + workspace_port: response_object, + errors: response.errors + } + end + end + end + end +end diff --git a/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb new file mode 100644 index 00000000000000..04b331e54ba8b0 --- /dev/null +++ b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Mutations + module RemoteDevelopment + module WorkspacePortsOperations + class Update < BaseMutation + graphql_name 'WorkspacePortUpdate' + + include Gitlab::Utils::UsageData + + authorize :update_workspace_port + + field :workspace_port, + Types::RemoteDevelopment::WorkspacePortType, + null: true, + description: 'Updated workspace port.' + + argument :id, + ::Types::GlobalIDType[::RemoteDevelopment::WorkspacePort], + required: true, + description: 'GlobalID of the workspace port.' + + argument :visibility, + GraphQL::Types::String, + required: true, + description: 'Visibility of the created port.' + + # @param [Hash] args + def resolve(id:, **args) + unless License.feature_available?(:remote_development) + raise_resource_not_available_error!("'remote_development' licensed feature is not available") + end + + workspace_port = authorized_find!(id: id) + + domain_main_class_args = { + current_user: current_user, + workspace_port: workspace_port, + params: args + } + + response = ::RemoteDevelopment::CommonService.execute( + domain_main_class: ::RemoteDevelopment::WorkspacePortsOperations::Update::Main, + domain_main_class_args: domain_main_class_args + ) + + response_object = response.success? ? response.payload[:workspace_port] : nil + + { + workspace_port: response_object, + errors: response.errors + } + end + end + end + end +end diff --git a/ee/app/graphql/types/remote_development/workspace_port_type.rb b/ee/app/graphql/types/remote_development/workspace_port_type.rb new file mode 100644 index 00000000000000..f02502501aaf55 --- /dev/null +++ b/ee/app/graphql/types/remote_development/workspace_port_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module RemoteDevelopment + class WorkspacePortType < ::Types::BaseObject + graphql_name 'WorkspacePort' + description 'Represents a remote development workspace port' + + authorize :read_workspace_port + + field :id, ::Types::GlobalIDType[::RemoteDevelopment::WorkspacePort], + null: false, description: 'Global ID of the workspace port.' + + field :workspace_id, GraphQL::Types::ID, + null: false, description: 'ID of the workspace to which the port belongs.' + + field :port_number, GraphQL::Types::Int, + null: false, description: 'Number of the workspace port.' + + field :visibility, GraphQL::Types::String, + null: false, description: 'Visibility status of the workspace port.' + + field :created_at, Types::TimeType, + null: false, description: 'Timestamp of when the workspace was created.' + + field :updated_at, Types::TimeType, + null: false, description: 'Timestamp of the last update to any mutable workspace property.' + end + 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 9e617c20503a58..47d9a88375cca2 100644 --- a/ee/app/graphql/types/remote_development/workspace_type.rb +++ b/ee/app/graphql/types/remote_development/workspace_type.rb @@ -101,6 +101,11 @@ class WorkspaceType < ::Types::BaseObject description: 'Forces all resources to be included for the workspace' \ 'during the next reconciliation with the agent.' + field :workspace_ports, [Types::RemoteDevelopment::WorkspacePortType], + null: true, + description: 'Ports exposed on the workspace.', + skip_type_authorization: :read_workspace_port + field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the workspace was created.' diff --git a/ee/app/models/remote_development/workspace.rb b/ee/app/models/remote_development/workspace.rb index d99e968e9bfd56..47928cc980890f 100644 --- a/ee/app/models/remote_development/workspace.rb +++ b/ee/app/models/remote_development/workspace.rb @@ -21,6 +21,7 @@ class Workspace < ApplicationRecord attribute :desired_config_generator_version, default: ::RemoteDevelopment::WorkspaceOperations::DesiredConfigGeneratorVersion::LATEST_VERSION + has_many :workspace_ports, class_name: 'RemoteDevelopment::WorkspacePort', inverse_of: :workspace 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, -> { diff --git a/ee/app/models/remote_development/workspace_port.rb b/ee/app/models/remote_development/workspace_port.rb new file mode 100644 index 00000000000000..3ae7f826c4a5bf --- /dev/null +++ b/ee/app/models/remote_development/workspace_port.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class WorkspacePort < ApplicationRecord + belongs_to :workspace, class_name: 'RemoteDevelopment::Workspace', inverse_of: :workspace_ports + + scope :open_ports, -> { + where(visibility: ::RemoteDevelopment::WorkspaceOperations::PortVisibility::WORKSPACE_PORT_STATES[:OPEN_PRIVATE]) + } + + validates :visibility, presence: true, inclusion: { + in: ::RemoteDevelopment::WorkspaceOperations::PortVisibility::WORKSPACE_PORT_STATES.values + } + validates :port_number, presence: true + end +end diff --git a/ee/app/policies/remote_development/workspace_policy.rb b/ee/app/policies/remote_development/workspace_policy.rb index 69b3f40b79aeef..bc08f094b7bd02 100644 --- a/ee/app/policies/remote_development/workspace_policy.rb +++ b/ee/app/policies/remote_development/workspace_policy.rb @@ -14,6 +14,7 @@ class WorkspacePolicy < BasePolicy rule { admin }.enable :read_workspace rule { admin }.enable :update_workspace + rule { admin }.enable :create_workspace_port rule { can_admin_owned_workspace }.enable :read_workspace rule { can_admin_owned_workspace }.enable :update_workspace diff --git a/ee/app/policies/remote_development/workspace_port_policy.rb b/ee/app/policies/remote_development/workspace_port_policy.rb new file mode 100644 index 00000000000000..a2c60504b32d17 --- /dev/null +++ b/ee/app/policies/remote_development/workspace_port_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module RemoteDevelopment + class WorkspacePortPolicy < BasePolicy + rule { admin }.enable :read_workspace_port + rule { admin }.enable :update_workspace_port + end +end diff --git a/ee/lib/remote_development/messages.rb b/ee/lib/remote_development/messages.rb index c5e531d44bd7c5..490ebbd9d2b9a9 100644 --- a/ee/lib/remote_development/messages.rb +++ b/ee/lib/remote_development/messages.rb @@ -20,11 +20,13 @@ module Messages WorkspaceCreateDevfileFlattenFailed = Class.new(Gitlab::Fp::Message) PersonalAccessTokenModelCreateFailed = Class.new(Gitlab::Fp::Message) WorkspaceModelCreateFailed = Class.new(Gitlab::Fp::Message) + WorkspacePortModelCreateFailed = Class.new(Gitlab::Fp::Message) WorkspaceVariablesModelCreateFailed = Class.new(Gitlab::Fp::Message) WorkspaceCreateFailed = Class.new(Gitlab::Fp::Message) # Workspace update errors WorkspaceUpdateFailed = Class.new(Gitlab::Fp::Message) + WorkspacePortUpdateFailed = Class.new(Gitlab::Fp::Message) # Workspace reconcile errors WorkspaceReconcileParamsValidationFailed = Class.new(Gitlab::Fp::Message) @@ -59,6 +61,8 @@ module Messages # Workspace domain events WorkspaceCreateSuccessful = Class.new(Gitlab::Fp::Message) WorkspaceUpdateSuccessful = Class.new(Gitlab::Fp::Message) + WorkspacePortCreateSuccessful = Class.new(Gitlab::Fp::Message) + WorkspacePortUpdateSuccessful = Class.new(Gitlab::Fp::Message) WorkspaceReconcileSuccessful = Class.new(Gitlab::Fp::Message) # Namespace Cluster Agent Mapping domain events diff --git a/ee/lib/remote_development/workspace_operations/create/creator.rb b/ee/lib/remote_development/workspace_operations/create/creator.rb index a785bda6a1dcf3..b4f3f911e4e9df 100644 --- a/ee/lib/remote_development/workspace_operations/create/creator.rb +++ b/ee/lib/remote_development/workspace_operations/create/creator.rb @@ -22,11 +22,12 @@ def self.create(context) .map(CreatorBootstrapper.method(:bootstrap)) .and_then(PersonalAccessTokenCreator.method(:create)) .and_then(WorkspaceCreator.method(:create)) + .and_then(WorkspaceDevfilePortsCreator.method(:create)) .and_then(WorkspaceVariablesCreator.method(:create)) case result in { err: PersonalAccessTokenModelCreateFailed | - WorkspaceModelCreateFailed | + WorkspaceModelCreateFailed | WorkspacePortsModelCreateFailed | WorkspaceVariablesModelCreateFailed => message } model_errors = message.content[:errors] diff --git a/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb b/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb new file mode 100644 index 00000000000000..b4a1518b390932 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module Create + class WorkspaceDevfilePortsCreator + include Messages + + # @param [Hash] context + # @return [Gitlab::Fp::Result] + def self.create(context) + context => { + workspace: RemoteDevelopment::Workspace => workspace, + processed_devfile: Hash => processed_devfile, + } + + processed_devfile.fetch('components').each do |devfile_component| + container_endpoints = devfile_component.dig('container', 'endpoints') + + next unless container_endpoints && !container_endpoints.empty? + + # Here we are removing the user specified port from the processed_devfile so that upon reconcilliation, + # it does not get added to the default service for the workspace where the editor and ssh server ports + # are exposed because we want to create a separate service for all user provided ports, this allows + # decoupling of both services and we can safely open/close ports that were extracted from the devfile + # by updating the user provided ports service. + container_endpoints.reject! do |endpoint| + endpoint_name = endpoint.fetch('name') + server_ports_check = endpoint_name != 'editor-server' && endpoint_name != 'ssh-server' + + if server_ports_check + port_number = endpoint.fetch('targetPort') + + workspace_port = RemoteDevelopment::WorkspacePort.new + workspace_port.workspace_id = workspace.id + workspace_port.port_number = port_number + workspace_port.user_provided = true + workspace_port.visibility = + ::RemoteDevelopment::WorkspaceOperations::PortVisibility::WORKSPACE_PORT_STATES[:OPEN_PRIVATE] + + workspace_port.save! + end + + server_ports_check + end + end + + workspace.processed_devfile = YAML.dump(processed_devfile.deep_stringify_keys) + workspace.save! + + Gitlab::Fp::Result.ok(context) + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/port_visibility.rb b/ee/lib/remote_development/workspace_operations/port_visibility.rb new file mode 100644 index 00000000000000..89ec13abfb4e21 --- /dev/null +++ b/ee/lib/remote_development/workspace_operations/port_visibility.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspaceOperations + module PortVisibility + WORKSPACE_PORT_STATES = { + OPEN_PRIVATE: 'private', + OPEN_PUBLIC: 'public', + CLOSED: 'closed' + }.freeze + end + end +end diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb index b97564e39c6b06..9492135161b8bf 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb @@ -6,6 +6,7 @@ module Reconcile module Output class DesiredConfigGenerator include ReconcileConstants + include PortVisibility # @param [RemoteDevelopment::Workspace] workspace # @param [Boolean] include_all_resources @@ -36,6 +37,8 @@ def self.generate_desired_config(workspace:, include_all_resources:, logger:) workspace_inventory_name: String => workspace_inventory_name, } + ports_service_name = "#{workspace.name}-ports-service" + desired_config = [] append_inventory_config_map( @@ -97,6 +100,16 @@ def self.generate_desired_config(workspace:, include_all_resources:, logger:) annotations: workspace_inventory_annotations ) + workspace_ports_service_definition = get_ports_service( + workspace_ports: workspace.workspace_ports.open_ports, + name: ports_service_name, + namespace: workspace.namespace, + labels: labels, + annotations: annotations, + agent_id: workspace.agent.id + ) + desired_config.append(workspace_ports_service_definition) + append_network_policy( desired_config: desired_config, name: workspace.name, @@ -389,7 +402,43 @@ def self.append_resource_quota( nil end - # @param [Array] desired_config + # @param [RemoteDevelopment::WorkspacePort] workspace_ports + # @param [String] name + # @param [String] namespace + # @param [Hash] labels + # @param [Hash] annotations + # @return [Hash] + def self.get_ports_service(workspace_ports:, name:, namespace:, labels:, annotations:, agent_id:) + ports = [] + + workspace_ports.map do |port| + ports.append({ + name: "open-port-#{port.port_number}", + port: port.port_number, + protocol: 'TCP', + targetPort: port.port_number + }) + end + + { + apiVersion: 'v1', + kind: 'Service', + metadata: { + name: name, + namespace: namespace, + annotations: annotations, + labels: labels + }, + spec: { + ports: ports, + selector: { + 'agent.gitlab.com/id' => agent_id.to_s + }, + type: 'ClusterIP' + } + }.deep_stringify_keys.to_h + end + # @param [String] name # @param [String] namespace # @param [Hash] labels diff --git a/ee/lib/remote_development/workspace_ports_operations/create/main.rb b/ee/lib/remote_development/workspace_ports_operations/create/main.rb new file mode 100644 index 00000000000000..51b6e0511fc135 --- /dev/null +++ b/ee/lib/remote_development/workspace_ports_operations/create/main.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspacePortsOperations + module Create + class Main + include Messages + extend Gitlab::Fp::MessageSupport + + # @param [Hash] context + # @return [Hash] + # @raise [Gitlab::Fp::UnmatchedResultError] + def self.main(context) + initial_result = Gitlab::Fp::Result.ok(context) + + result = + initial_result + .and_then(PortCreator.method(:create)) + + case result + in { ok: WorkspacePortCreateSuccessful => message } + { status: :success, payload: message.content } + else + raise Gitlab::Fp::UnmatchedResultError.new(result: result) + end + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_ports_operations/create/port_creator.rb b/ee/lib/remote_development/workspace_ports_operations/create/port_creator.rb new file mode 100644 index 00000000000000..ed545cbb840610 --- /dev/null +++ b/ee/lib/remote_development/workspace_ports_operations/create/port_creator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspacePortsOperations + module Create + class PortCreator + include Messages + + # @param [Hash] context + # @return [Gitlab::Fp::Result] + def self.create(context) + context => { + workspace: RemoteDevelopment::Workspace => workspace, + params: Hash => params, + } + + workspace_port = RemoteDevelopment::WorkspacePort.new + workspace_port.port_number = params.fetch(:port_number) + workspace_port.visibility = params.fetch(:visibility) + workspace_port.workspace = workspace + + workspace_port.save! + + workspace.touch(:desired_state_updated_at) + + if workspace_port.errors.present? + return Gitlab::Fp::Result.err( + WorkspacePortModelCreateFailed.new({ errors: workspace_port.errors }) + ) + end + + Gitlab::Fp::Result.ok(WorkspacePortCreateSuccessful.new({ + workspace_port: workspace_port + })) + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_ports_operations/update/main.rb b/ee/lib/remote_development/workspace_ports_operations/update/main.rb new file mode 100644 index 00000000000000..f2534df19b3da4 --- /dev/null +++ b/ee/lib/remote_development/workspace_ports_operations/update/main.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspacePortsOperations + module Update + class Main + include Messages + extend Gitlab::Fp::MessageSupport + + # @param [Hash] context + # @return [Hash] + # @raise [Gitlab::Fp::UnmatchedResultError] + def self.main(context) + initial_result = Gitlab::Fp::Result.ok(context) + + result = + initial_result + .and_then(Updater.method(:update)) + + case result + in { ok: WorkspacePortUpdateSuccessful => message } + { status: :success, payload: message.content } + else + raise Gitlab::Fp::UnmatchedResultError.new(result: result) + end + end + end + end + end +end diff --git a/ee/lib/remote_development/workspace_ports_operations/update/updater.rb b/ee/lib/remote_development/workspace_ports_operations/update/updater.rb new file mode 100644 index 00000000000000..9a9bfbc5a60068 --- /dev/null +++ b/ee/lib/remote_development/workspace_ports_operations/update/updater.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module RemoteDevelopment + module WorkspacePortsOperations + module Update + class Updater + include Messages + + # @param [Hash] context + # @return [Gitlab::Fp::Result] + def self.update(context) + context => { workspace_port: RemoteDevelopment::WorkspacePort => workspace_port, params: Hash => params } + + workspace_port.update(params) + + workspace_port.workspace.touch(:desired_state_updated_at) + + Gitlab::Fp::Result.ok(WorkspacePortUpdateSuccessful.new({ workspace_port: workspace_port })) + end + end + end + end +end diff --git a/ee/spec/factories/remote_development/workspace_ports.rb b/ee/spec/factories/remote_development/workspace_ports.rb new file mode 100644 index 00000000000000..6b3cf613f71355 --- /dev/null +++ b/ee/spec/factories/remote_development/workspace_ports.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :workspace_ports, class: 'RemoteDevelopment::WorkspacePort' do + workspace { association(:workspace) } + port_number { generate(:port_number) } + visibility { generate(:visibility) } + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4fe9cf116a6acc..dc43f97b4b7e58 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -69170,6 +69170,10 @@ msgid "WorkItem|ticket" msgstr "" msgid "Workflow" +msgid "Workspace Ports|Closed" +msgstr "" + +msgid "Workspace Ports|Open [Private]" msgstr "" msgid "Workspaces" @@ -69220,6 +69224,9 @@ msgstr "" msgid "Workspaces|Cancel" msgstr "" +msgid "Workspaces|Close Port" +msgstr "" + msgid "Workspaces|Cluster agent" msgstr "" @@ -69313,9 +69320,15 @@ msgstr "" msgid "Workspaces|Open workspace" msgstr "" +msgid "Workspaces|Open workspace Port" +msgstr "" + msgid "Workspaces|Path to devfile" msgstr "" +msgid "Workspaces|Port Number" +msgstr "" + msgid "Workspaces|Project" msgstr "" @@ -69415,6 +69428,21 @@ msgstr "" msgid "Workspaces|Your workspaces" msgstr "" +msgid "Workspace|Access" +msgstr "" + +msgid "Workspace|Port" +msgstr "" + +msgid "Workspace|Workspace Ports" +msgstr "" + +msgid "Would you like to create a new branch?" +msgstr "" + +msgid "Would you like to try auto-generating a branch name?" +msgstr "" + msgid "Write" msgstr "" -- GitLab From 780456a938e06df035630c63705d3aafb87e9da3 Mon Sep 17 00:00:00 2001 From: Daniyal Arshad Date: Wed, 22 Jan 2025 17:22:06 -0500 Subject: [PATCH 2/6] backfill migration for workspace_ports --- ...20250122203722_backfill_workspace_ports.rb | 54 +++++++++++++++++++ db/schema_migrations/20250122203722 | 1 + .../workspace_ports_operations/create.rb | 5 +- .../remote_development/workspace_type.rb | 2 +- .../create/workspace_devfile_ports_creator.rb | 8 +-- 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20250122203722_backfill_workspace_ports.rb create mode 100644 db/schema_migrations/20250122203722 diff --git a/db/migrate/20250122203722_backfill_workspace_ports.rb b/db/migrate/20250122203722_backfill_workspace_ports.rb new file mode 100644 index 00000000000000..be35bd6e936e5d --- /dev/null +++ b/db/migrate/20250122203722_backfill_workspace_ports.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class BackfillWorkspacePorts < Gitlab::Database::Migration[2.2] + milestone '17.9' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class Workspace < MigrationRecord + self.table_name = :workspaces + + include EachBatch + end + + class WorkspacePort < MigrationRecord + self.table_name = :workspace_ports + end + + BATCH_SIZE = 100 + + def up + Workspace.reset_column_information + + Workspace.each_batch(of: BATCH_SIZE) do |batch| + workspaces = batch.where.not(desired_state: "Terminated").pluck(:id, :processed_devfile) + + workspaces.each do |id, processed_devfile| + workspace_ports = extract_ports_from_processed_devfile(processed_devfile, id) + + WorkspacePort.insert_all(workspace_ports) if workspace_ports + end + end + end + + def down + # no-op + end + + private + + def extract_ports_from_processed_devfile(processed_devfile, workspace_id) + processed_devfile = YAML.safe_load(processed_devfile) + main_component = processed_devfile + .fetch("components") + .find { |component| component.dig("attributes", "gl/inject-editor") } + + endpoints = main_component.dig("container", "endpoints") + + return if endpoints.nil? + + endpoints.map do |endpoint| + { workspace_id: workspace_id, port_number: endpoint["targetPort"] } + end + end +end diff --git a/db/schema_migrations/20250122203722 b/db/schema_migrations/20250122203722 new file mode 100644 index 00000000000000..a5d92ba74bec67 --- /dev/null +++ b/db/schema_migrations/20250122203722 @@ -0,0 +1 @@ +f69d3dadedf4be0be5b77f3d5a31ee07963ed86dc5e4efa9c77709241b214554 \ No newline at end of file diff --git a/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb index aa5b870be64153..8d7225449b31f6 100644 --- a/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb +++ b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/create.rb @@ -47,7 +47,10 @@ def resolve(args) response = ::RemoteDevelopment::CommonService.execute( domain_main_class: ::RemoteDevelopment::WorkspacePortsOperations::Create::Main, - domain_main_class_args: domain_main_class_args + domain_main_class_args: domain_main_class_args, + auth_ability: :create_workspace_port, + auth_subject: workspace, + current_user: current_user ) response_object = response.success? ? response.payload[:workspace_port] : nil diff --git a/ee/app/graphql/types/remote_development/workspace_type.rb b/ee/app/graphql/types/remote_development/workspace_type.rb index 47d9a88375cca2..c4a56f37ba0c9d 100644 --- a/ee/app/graphql/types/remote_development/workspace_type.rb +++ b/ee/app/graphql/types/remote_development/workspace_type.rb @@ -112,7 +112,7 @@ 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, + field :workspace_variables, # rubocop:disable GraphQL/ExtractType -- Not needed for now ::Types::RemoteDevelopment::WorkspaceVariableType.connection_type, null: true, experiment: { milestone: '17.9' }, diff --git a/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb b/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb index b4a1518b390932..2d5bae72520658 100644 --- a/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb +++ b/ee/lib/remote_development/workspace_operations/create/workspace_devfile_ports_creator.rb @@ -14,8 +14,8 @@ def self.create(context) processed_devfile: Hash => processed_devfile, } - processed_devfile.fetch('components').each do |devfile_component| - container_endpoints = devfile_component.dig('container', 'endpoints') + processed_devfile.fetch(:components).each do |devfile_component| + container_endpoints = devfile_component.dig(:container, :endpoints) next unless container_endpoints && !container_endpoints.empty? @@ -25,11 +25,11 @@ def self.create(context) # decoupling of both services and we can safely open/close ports that were extracted from the devfile # by updating the user provided ports service. container_endpoints.reject! do |endpoint| - endpoint_name = endpoint.fetch('name') + endpoint_name = endpoint.fetch(:name) server_ports_check = endpoint_name != 'editor-server' && endpoint_name != 'ssh-server' if server_ports_check - port_number = endpoint.fetch('targetPort') + port_number = endpoint.fetch(:targetPort) workspace_port = RemoteDevelopment::WorkspacePort.new workspace_port.workspace_id = workspace.id -- GitLab From d4d0e543667999def2adb75ed7824334760961da Mon Sep 17 00:00:00 2001 From: Daniyal Arshad Date: Wed, 29 Jan 2025 18:57:26 -0500 Subject: [PATCH 3/6] Ad url method to workspace_ports --- doc/api/graphql/reference/_index.md | 1 + .../graphql/types/remote_development/workspace_port_type.rb | 4 ++++ ee/app/models/remote_development/workspace_port.rb | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index af127473007fd9..fd77364a38ad13 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -42114,6 +42114,7 @@ Represents a remote development workspace port. | `id` | [`RemoteDevelopmentWorkspacePortID!`](#remotedevelopmentworkspaceportid) | Global ID of the workspace port. | | `portNumber` | [`Int!`](#int) | Number of the workspace port. | | `updatedAt` | [`Time!`](#time) | Timestamp of the last update to any mutable workspace property. | +| `url` | [`String!`](#string) | URL to access the application running on the workspace port. | | `visibility` | [`String!`](#string) | Visibility status of the workspace port. | | `workspaceId` | [`ID!`](#id) | ID of the workspace to which the port belongs. | diff --git a/ee/app/graphql/types/remote_development/workspace_port_type.rb b/ee/app/graphql/types/remote_development/workspace_port_type.rb index f02502501aaf55..811329c57b33e6 100644 --- a/ee/app/graphql/types/remote_development/workspace_port_type.rb +++ b/ee/app/graphql/types/remote_development/workspace_port_type.rb @@ -20,6 +20,10 @@ class WorkspacePortType < ::Types::BaseObject field :visibility, GraphQL::Types::String, null: false, description: 'Visibility status of the workspace port.' + field :url, GraphQL::Types::String, + null: false, + description: 'URL to access the application running on the workspace port.', method: :access_url + field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the workspace was created.' diff --git a/ee/app/models/remote_development/workspace_port.rb b/ee/app/models/remote_development/workspace_port.rb index 3ae7f826c4a5bf..5503070bfdcf03 100644 --- a/ee/app/models/remote_development/workspace_port.rb +++ b/ee/app/models/remote_development/workspace_port.rb @@ -12,5 +12,9 @@ class WorkspacePort < ApplicationRecord in: ::RemoteDevelopment::WorkspaceOperations::PortVisibility::WORKSPACE_PORT_STATES.values } validates :port_number, presence: true + + def access_url + "https://#{port_number}-#{workspace.name}.workspaces.localdev.me" + end end end -- GitLab From 68fdc23cfefc546c09912a430bed68cada1a36a9 Mon Sep 17 00:00:00 2001 From: Daniyal Arshad Date: Tue, 4 Feb 2025 21:25:37 -0500 Subject: [PATCH 4/6] Add WORKSPACE_ID env var to inject into workspace --- .../workspace_ports_operations/update.rb | 5 ++++- .../graphql/types/remote_development/workspace_type.rb | 1 + ee/app/models/remote_development/workspace.rb | 9 ++++++--- ee/app/policies/remote_development/workspace_policy.rb | 1 + .../reconcile/output/desired_config_generator.rb | 2 ++ 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb index 04b331e54ba8b0..5ee699757a4a5f 100644 --- a/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb +++ b/ee/app/graphql/mutations/remote_development/workspace_ports_operations/update.rb @@ -41,7 +41,10 @@ def resolve(id:, **args) response = ::RemoteDevelopment::CommonService.execute( domain_main_class: ::RemoteDevelopment::WorkspacePortsOperations::Update::Main, - domain_main_class_args: domain_main_class_args + domain_main_class_args: domain_main_class_args, + auth_ability: :update_workspace_port, + auth_subject: workspace_port.workspace, + current_user: current_user ) response_object = response.success? ? response.payload[:workspace_port] : nil diff --git a/ee/app/graphql/types/remote_development/workspace_type.rb b/ee/app/graphql/types/remote_development/workspace_type.rb index c4a56f37ba0c9d..fb164a9e36bf77 100644 --- a/ee/app/graphql/types/remote_development/workspace_type.rb +++ b/ee/app/graphql/types/remote_development/workspace_type.rb @@ -103,6 +103,7 @@ class WorkspaceType < ::Types::BaseObject field :workspace_ports, [Types::RemoteDevelopment::WorkspacePortType], null: true, + method: :open_workspace_ports, description: 'Ports exposed on the workspace.', skip_type_authorization: :read_workspace_port diff --git a/ee/app/models/remote_development/workspace.rb b/ee/app/models/remote_development/workspace.rb index 47928cc980890f..630a1001503c76 100644 --- a/ee/app/models/remote_development/workspace.rb +++ b/ee/app/models/remote_development/workspace.rb @@ -22,11 +22,14 @@ class Workspace < ApplicationRecord default: ::RemoteDevelopment::WorkspaceOperations::DesiredConfigGeneratorVersion::LATEST_VERSION has_many :workspace_ports, class_name: 'RemoteDevelopment::WorkspacePort', inverse_of: :workspace - has_many :workspace_variables, class_name: "RemoteDevelopment::WorkspaceVariable", inverse_of: :workspace - # Currently we only support :environment type for user provided variables + has_many :workspace_variables, class_name: 'RemoteDevelopment::WorkspaceVariable', inverse_of: :workspace has_many :user_provided_workspace_variables, -> { user_provided.with_variable_type_environment.order_id_desc - }, class_name: "RemoteDevelopment::WorkspaceVariable", inverse_of: :workspace + }, class_name: 'RemoteDevelopment::WorkspaceVariable', inverse_of: :workspace + # Currently we only support :environment type for user provided variables + has_many :open_workspace_ports, -> { + open_ports + }, class_name: 'RemoteDevelopment::WorkspacePort', inverse_of: :workspace validates :user, presence: true validates :agent, presence: true diff --git a/ee/app/policies/remote_development/workspace_policy.rb b/ee/app/policies/remote_development/workspace_policy.rb index bc08f094b7bd02..7e3727ae9b503d 100644 --- a/ee/app/policies/remote_development/workspace_policy.rb +++ b/ee/app/policies/remote_development/workspace_policy.rb @@ -15,6 +15,7 @@ class WorkspacePolicy < BasePolicy rule { admin }.enable :read_workspace rule { admin }.enable :update_workspace rule { admin }.enable :create_workspace_port + rule { admin }.enable :update_workspace_port rule { can_admin_owned_workspace }.enable :read_workspace rule { can_admin_owned_workspace }.enable :update_workspace diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb index 9492135161b8bf..76c348ade4df5b 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb @@ -241,6 +241,8 @@ def self.append_secret_data_from_variables(desired_config:, secret_name:, variab hash[workspace_variable.key.to_sym] = workspace_variable.value end + data["WORKSPACE_ID"] = workspace.id.to_s + append_secret_data( desired_config: desired_config, secret_name: secret_name, -- GitLab From 736884c9e60a0a01ef62e1d4d1bd21437165e2e4 Mon Sep 17 00:00:00 2001 From: Daniyal Arshad Date: Wed, 5 Feb 2025 00:38:42 -0500 Subject: [PATCH 5/6] Use dev build image for tools injector --- doc/api/graphql/reference/_index.md | 26 +++++---- .../tools_injector_component_inserter.rb | 6 +-- .../output/desired_config_generator.rb | 54 ++++++++++--------- locale/gitlab.pot | 2 + 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index fd77364a38ad13..d124285b2757d4 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -42085,23 +42085,10 @@ 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. | +| `workspacePorts` | [`[WorkspacePort!]`](#workspaceport) | Ports exposed on the workspace. | | `workspaceVariables` {{< icon name="warning-solid" >}} | [`WorkspaceVariableConnection`](#workspacevariableconnection) | **Introduced** in GitLab 17.9. **Status**: Experiment. User defined variables associated with the workspace. | | `workspacesAgentConfigVersion` {{< icon name="warning-solid" >}} | [`Int!`](#int) | **Introduced** in GitLab 17.6. **Status**: Experiment. Version of the associated WorkspacesAgentConfig for the workspace. | -### `WorkspaceResources` - -Resource specifications of the workspace container. - -#### Fields - -| Name | Type | Description | -| ---- | ---- | ----------- | -| `limits` | [`ResourceQuotas`](#resourcequotas) | Limits for the requested container resources of a workspace. | -| `requests` | [`ResourceQuotas`](#resourcequotas) | Requested resources for the container of a workspace. | -| `workspacePorts` | [`[WorkspacePort!]`](#workspaceport) | Ports exposed on 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. | - ### `WorkspacePort` Represents a remote development workspace port. @@ -42118,6 +42105,17 @@ Represents a remote development workspace port. | `visibility` | [`String!`](#string) | Visibility status of the workspace port. | | `workspaceId` | [`ID!`](#id) | ID of the workspace to which the port belongs. | +### `WorkspaceResources` + +Resource specifications of the workspace container. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `limits` | [`ResourceQuotas`](#resourcequotas) | Limits for the requested container resources of a workspace. | +| `requests` | [`ResourceQuotas`](#resourcequotas) | Requested resources for the container of a workspace. | + ### `WorkspaceVariable` Represents a remote development workspace variable. diff --git a/ee/lib/remote_development/workspace_operations/create/tools_injector_component_inserter.rb b/ee/lib/remote_development/workspace_operations/create/tools_injector_component_inserter.rb index ff3057abc3d7d0..04bcf6daf29026 100644 --- a/ee/lib/remote_development/workspace_operations/create/tools_injector_component_inserter.rb +++ b/ee/lib/remote_development/workspace_operations/create/tools_injector_component_inserter.rb @@ -30,11 +30,11 @@ def self.insert(context) # @param [String] tools_dir # @param [String] image # @return [void] - def self.insert_tools_injector_component(processed_devfile:, tools_dir:, image:) - processed_devfile.fetch(:components) << { + def self.insert_tools_injector_component(processed_devfile:, tools_dir:, image:) # rubocop:disable Lint/UnusedMethodArgument -- Ignored for now + processed_devfile[:components] << { name: TOOLS_INJECTOR_COMPONENT_NAME, container: { - image: image, + image: "registry.gitlab.com/gitlab-org/workspaces/gitlab-workspaces-tools:dev-20250205034551", env: [ { name: TOOLS_DIR_ENV_VAR, diff --git a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb index 76c348ade4df5b..e1b785feecf835 100644 --- a/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb +++ b/ee/lib/remote_development/workspace_operations/reconcile/output/desired_config_generator.rb @@ -12,30 +12,34 @@ class DesiredConfigGenerator # @param [Boolean] include_all_resources # @param [RemoteDevelopment::Logger] logger # @return [Array] - def self.generate_desired_config(workspace:, include_all_resources:, logger:) - config_values_extractor_result = ConfigValuesExtractor.extract(workspace: workspace) - config_values_extractor_result => { - allow_privilege_escalation: TrueClass | FalseClass => allow_privilege_escalation, - common_annotations: Hash => common_annotations, - default_resources_per_workspace_container: Hash => default_resources_per_workspace_container, - default_runtime_class: String => default_runtime_class, - domain_template: String => domain_template, - env_secret_name: String => env_secret_name, - file_secret_name: String => file_secret_name, - gitlab_workspaces_proxy_namespace: String => gitlab_workspaces_proxy_namespace, - image_pull_secrets: Array => image_pull_secrets, - labels: Hash => labels, - max_resources_per_workspace: Hash => max_resources_per_workspace, - network_policy_enabled: TrueClass | FalseClass => network_policy_enabled, - network_policy_egress: Array => network_policy_egress, - processed_devfile_yaml: String => processed_devfile_yaml, - replicas: Integer => replicas, - secrets_inventory_annotations: Hash => secrets_inventory_annotations, - secrets_inventory_name: String => secrets_inventory_name, - use_kubernetes_user_namespaces: TrueClass | FalseClass => use_kubernetes_user_namespaces, - workspace_inventory_annotations: Hash => workspace_inventory_annotations, - workspace_inventory_name: String => workspace_inventory_name, - } + def self.generate_desired_config(workspace:, include_all_resources:, logger:) # rubocop:disable Metrics/AbcSize -- ignored + # NOTE: update env_secret_name to "#{workspace.name}-environment". This is to ensure naming consistency. + # Changing it now would require migration from old config version to a new one. + # Update this when a new desired config generator is created for some other reason. + env_secret_name = "#{workspace.name}-env-var" + file_secret_name = "#{workspace.name}-file" + workspaces_agent_config = workspace.workspaces_agent_config + + max_resources_per_workspace = workspaces_agent_config.max_resources_per_workspace.deep_symbolize_keys + + agent_annotations = workspaces_agent_config.annotations + domain_template = "{{.port}}-#{workspace.name}.#{workspaces_agent_config.dns_zone}" + common_annotations = get_common_annotations( + agent_annotations: agent_annotations, + domain_template: domain_template, + workspace_id: workspace.id, + max_resources_per_workspace: max_resources_per_workspace + ) + + labels = workspaces_agent_config.labels.merge({ "agent.gitlab.com/id": workspace.agent.id.to_s }) + + workspace_inventory_name = "#{workspace.name}-workspace-inventory" + workspace_inventory_annotations = + common_annotations.merge("config.k8s.io/owning-inventory": workspace_inventory_name) + + secrets_inventory_name = "#{workspace.name}-secrets-inventory" + secrets_inventory_annotations = + common_annotations.merge("config.k8s.io/owning-inventory": secrets_inventory_name) ports_service_name = "#{workspace.name}-ports-service" @@ -241,7 +245,7 @@ def self.append_secret_data_from_variables(desired_config:, secret_name:, variab hash[workspace_variable.key.to_sym] = workspace_variable.value end - data["WORKSPACE_ID"] = workspace.id.to_s + data["WORKSPACE_ID"] = "gid://gitlab/RemoteDevelopment::Workspace/#{workspace.id}" append_secret_data( desired_config: desired_config, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index dc43f97b4b7e58..6b4106e36ea061 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -69170,6 +69170,8 @@ msgid "WorkItem|ticket" msgstr "" msgid "Workflow" +msgstr "" + msgid "Workspace Ports|Closed" msgstr "" -- GitLab From b1fff261e13afebc216597ef6026cf4aec0190fc Mon Sep 17 00:00:00 2001 From: Daniyal Arshad Date: Sat, 5 Apr 2025 21:01:49 -0400 Subject: [PATCH 6/6] fix lint --- .../workspaces/common/components/workspace_ports.vue | 6 ------ locale/gitlab.pot | 6 ------ 2 files changed, 12 deletions(-) diff --git a/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue b/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue index 42caa094b5e59c..011a29dd20c612 100644 --- a/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue +++ b/ee/app/assets/javascripts/workspaces/common/components/workspace_ports.vue @@ -43,18 +43,12 @@ export default { }, data() { return { - workspacePorts: [], portNumber: 3000, isCreatingWorkspacePort: false, openPort: WORKSPACE_STATES.portOpened, closedPort: WORKSPACE_STATES.portClosed, }; }, - computed: { - openPorts() { - return this.clusterAgents.length === 0; - }, - }, methods: { async createWorkspacePort(workspaceId) { try { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6b4106e36ea061..0a0f59f11c3a57 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -69439,12 +69439,6 @@ msgstr "" msgid "Workspace|Workspace Ports" msgstr "" -msgid "Would you like to create a new branch?" -msgstr "" - -msgid "Would you like to try auto-generating a branch name?" -msgstr "" - msgid "Write" msgstr "" -- GitLab