From b4a358fe23cc75db6e17ddabb76e0f473eb01a42 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Mon, 29 Sep 2025 16:17:12 +0200 Subject: [PATCH 01/21] Generate protos --- ee/ephemeral_environments/Makefile | 2 +- .../internal_api/ephemeral_environments.pb.ex | 321 ++++++++++++++++++ .../include/google/protobuf/timestamp.pb.ex | 8 + .../scripts/internal_protos.sh | 9 + 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex create mode 100644 ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex create mode 100755 ee/ephemeral_environments/scripts/internal_protos.sh diff --git a/ee/ephemeral_environments/Makefile b/ee/ephemeral_environments/Makefile index b997d1f0b..33a326819 100644 --- a/ee/ephemeral_environments/Makefile +++ b/ee/ephemeral_environments/Makefile @@ -3,7 +3,7 @@ export MIX_ENV?=dev include ../../Makefile APP_NAME := $(shell grep 'app:' mix.exs | cut -d ':' -f3 | cut -d ',' -f1) -INTERNAL_API_BRANCH?=master +INTERNAL_API_BRANCH=master PROTOC_TAG=1.18.4-3.20.1-0.13.0 TMP_REPO_DIR?=/tmp/internal_api diff --git a/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex new file mode 100644 index 000000000..ce170d882 --- /dev/null +++ b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex @@ -0,0 +1,321 @@ +defmodule InternalApi.EphemeralEnvironments.InstanceState do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :INSTANCE_STATE_UNSPECIFIED, 0 + field :INSTANCE_STATE_ZERO_STATE, 1 + field :INSTANCE_STATE_PROVISIONING, 2 + field :INSTANCE_STATE_READY_TO_USE, 3 + field :INSTANCE_STATE_SLEEP, 4 + field :INSTANCE_STATE_IN_USE, 5 + field :INSTANCE_STATE_DEPLOYING, 6 + field :INSTANCE_STATE_DEPROVISIONING, 7 + field :INSTANCE_STATE_DESTROYED, 8 + field :INSTANCE_STATE_ACKNOWLEDGED_CLEANUP, 9 + field :INSTANCE_STATE_FAILED_PROVISIONING, 10 + field :INSTANCE_STATE_FAILED_DEPROVISIONING, 11 + field :INSTANCE_STATE_FAILED_DEPLOYMENT, 12 + field :INSTANCE_STATE_FAILED_CLEANUP, 13 + field :INSTANCE_STATE_FAILED_SLEEP, 14 + field :INSTANCE_STATE_FAILED_WAKE_UP, 15 +end + +defmodule InternalApi.EphemeralEnvironments.StateChangeActionType do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :STATE_CHANGE_ACTION_TYPE_UNSPECIFIED, 0 + field :STATE_CHANGE_ACTION_TYPE_PROVISIONING, 1 + field :STATE_CHANGE_ACTION_TYPE_CLEANUP, 2 + field :STATE_CHANGE_ACTION_TYPE_TO_SLEEP, 3 + field :STATE_CHANGE_ACTION_TYPE_WAKE_UP, 4 + field :STATE_CHANGE_ACTION_TYPE_DEPLOYING, 5 + field :STATE_CHANGE_ACTION_TYPE_CLEANING_UP, 6 + field :STATE_CHANGE_ACTION_TYPE_DEPROVISIONING, 7 +end + +defmodule InternalApi.EphemeralEnvironments.StateChangeResult do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :STATE_CHANGE_RESULT_UNSPECIFIED, 0 + field :STATE_CHANGE_RESULT_PASSED, 1 + field :STATE_CHANGE_RESULT_PENDING, 2 + field :STATE_CHANGE_RESULT_FAILED, 3 +end + +defmodule InternalApi.EphemeralEnvironments.TriggererType do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :TRIGGERER_TYPE_UNSPECIFIED, 0 + field :TRIGGERER_TYPE_USER, 1 + field :TRIGGERER_TYPE_AUTOMATION_RULE, 2 +end + +defmodule InternalApi.EphemeralEnvironments.TypeState do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :TYPE_STATE_UNSPECIFIED, 0 + field :TYPE_STATE_DRAFT, 1 + field :TYPE_STATE_READY, 2 + field :TYPE_STATE_CORDONED, 3 + field :TYPE_STATE_DELETED, 4 +end + +defmodule InternalApi.EphemeralEnvironments.ListRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :org_id, 1, type: :string, json_name: "orgId" + field :project_id, 2, type: :string, json_name: "projectId" +end + +defmodule InternalApi.EphemeralEnvironments.ListResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_types, 1, + repeated: true, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentTypes" +end + +defmodule InternalApi.EphemeralEnvironments.DescribeRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :org_id, 2, type: :string, json_name: "orgId" +end + +defmodule InternalApi.EphemeralEnvironments.DescribeResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" + + field :instances, 2, + repeated: true, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentInstance +end + +defmodule InternalApi.EphemeralEnvironments.CreateRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" +end + +defmodule InternalApi.EphemeralEnvironments.CreateResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" +end + +defmodule InternalApi.EphemeralEnvironments.UpdateRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" +end + +defmodule InternalApi.EphemeralEnvironments.UpdateResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" +end + +defmodule InternalApi.EphemeralEnvironments.DeleteRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :org_id, 2, type: :string, json_name: "orgId" +end + +defmodule InternalApi.EphemeralEnvironments.DeleteResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" +end + +defmodule InternalApi.EphemeralEnvironments.CordonRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :org_id, 2, type: :string, json_name: "orgId" +end + +defmodule InternalApi.EphemeralEnvironments.CordonResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" +end + +defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentType do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :org_id, 2, type: :string, json_name: "orgId" + field :name, 3, type: :string + field :description, 4, type: :string + field :created_by, 5, type: :string, json_name: "createdBy" + field :last_updated_by, 6, type: :string, json_name: "lastUpdatedBy" + field :created_at, 7, type: Google.Protobuf.Timestamp, json_name: "createdAt" + field :updated_at, 8, type: Google.Protobuf.Timestamp, json_name: "updatedAt" + field :state, 9, type: InternalApi.EphemeralEnvironments.TypeState, enum: true + field :max_number_of_instances, 10, type: :int32, json_name: "maxNumberOfInstances" +end + +defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentInstance do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :ee_type_id, 2, type: :string, json_name: "eeTypeId" + field :name, 3, type: :string + field :state, 4, type: InternalApi.EphemeralEnvironments.InstanceState, enum: true + field :last_state_change_id, 5, type: :string, json_name: "lastStateChangeId" + field :created_at, 6, type: Google.Protobuf.Timestamp, json_name: "createdAt" + field :updated_at, 7, type: Google.Protobuf.Timestamp, json_name: "updatedAt" +end + +defmodule InternalApi.EphemeralEnvironments.EphemeralSecretDefinition do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :ee_type_id, 2, type: :string, json_name: "eeTypeId" + field :name, 3, type: :string + field :description, 4, type: :string + + field :actions_that_can_change_the_secret, 5, + repeated: true, + type: InternalApi.EphemeralEnvironments.StateChangeAction, + json_name: "actionsThatCanChangeTheSecret" + + field :actions_that_have_access_to_the_secret, 6, + repeated: true, + type: InternalApi.EphemeralEnvironments.StateChangeAction, + json_name: "actionsThatHaveAccessToTheSecret" +end + +defmodule InternalApi.EphemeralEnvironments.StateChangeAction do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :type, 2, type: InternalApi.EphemeralEnvironments.StateChangeActionType, enum: true + field :project_id, 3, type: :string, json_name: "projectId" + field :branch, 4, type: :string + field :pipeline_yaml_name, 5, type: :string, json_name: "pipelineYamlName" + field :execution_auth_rules, 6, type: :string, json_name: "executionAuthRules" +end + +defmodule InternalApi.EphemeralEnvironments.InstanceStateChange do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :id, 1, type: :string + field :instance_id, 2, type: :string, json_name: "instanceId" + + field :prev_state, 3, + type: InternalApi.EphemeralEnvironments.InstanceState, + json_name: "prevState", + enum: true + + field :next_state, 4, + type: InternalApi.EphemeralEnvironments.InstanceState, + json_name: "nextState", + enum: true + + field :state_change_action, 5, + type: InternalApi.EphemeralEnvironments.StateChangeAction, + json_name: "stateChangeAction" + + field :result, 6, type: InternalApi.EphemeralEnvironments.StateChangeResult, enum: true + field :TriggererType, 7, type: :string + field :trigger_id, 8, type: :string, json_name: "triggerId" + field :execution_pipeline_id, 9, type: :string, json_name: "executionPipelineId" + field :execution_workflow_id, 10, type: :string, json_name: "executionWorkflowId" + field :started_at, 11, type: Google.Protobuf.Timestamp, json_name: "startedAt" + field :finished_at, 12, type: Google.Protobuf.Timestamp, json_name: "finishedAt" +end + +defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service do + @moduledoc false + + use GRPC.Service, + name: "InternalApi.EphemeralEnvironments.EphemeralEnvironments", + protoc_gen_elixir_version: "0.13.0" + + rpc :List, + InternalApi.EphemeralEnvironments.ListRequest, + InternalApi.EphemeralEnvironments.ListResponse + + rpc :Describe, + InternalApi.EphemeralEnvironments.DescribeRequest, + InternalApi.EphemeralEnvironments.DescribeResponse + + rpc :Create, + InternalApi.EphemeralEnvironments.CreateRequest, + InternalApi.EphemeralEnvironments.CreateResponse + + rpc :Update, + InternalApi.EphemeralEnvironments.UpdateRequest, + InternalApi.EphemeralEnvironments.UpdateResponse + + rpc :Delete, + InternalApi.EphemeralEnvironments.DeleteRequest, + InternalApi.EphemeralEnvironments.DeleteResponse + + rpc :Cordon, + InternalApi.EphemeralEnvironments.CordonRequest, + InternalApi.EphemeralEnvironments.CordonResponse +end + +defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironments.Stub do + @moduledoc false + + use GRPC.Stub, service: InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service +end \ No newline at end of file diff --git a/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex b/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex new file mode 100644 index 000000000..d7e0566d9 --- /dev/null +++ b/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex @@ -0,0 +1,8 @@ +defmodule Google.Protobuf.Timestamp do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field :seconds, 1, type: :int64 + field :nanos, 2, type: :int32 +end \ No newline at end of file diff --git a/ee/ephemeral_environments/scripts/internal_protos.sh b/ee/ephemeral_environments/scripts/internal_protos.sh new file mode 100755 index 000000000..6f930a097 --- /dev/null +++ b/ee/ephemeral_environments/scripts/internal_protos.sh @@ -0,0 +1,9 @@ +list=' +include/google/protobuf/timestamp +ephemeral_environments +' + +for element in $list;do + echo "$element" + protoc -I /home/protoc/source -I /home/protoc/source/include --elixir_out=plugins=grpc:/home/protoc/code/lib/internal_api --plugin=/root/.mix/escripts/protoc-gen-elixir /home/protoc/source/$element.proto +done \ No newline at end of file From a56df83de94a4ee0cb459faa7424e847ae88f31d Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Tue, 30 Sep 2025 12:41:21 +0200 Subject: [PATCH 02/21] Add empty server implementation, tested locally --- ee/ephemeral_environments/Makefile | 3 ++ .../lib/ephemeral_environments/application.ex | 11 ++++- .../repo/ephemeral_environment_type.ex | 30 +++++++++++++ .../lib/grpc/ephemeral_environments_server.ex | 42 +++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex create mode 100644 ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex diff --git a/ee/ephemeral_environments/Makefile b/ee/ephemeral_environments/Makefile index 33a326819..7e07dfa25 100644 --- a/ee/ephemeral_environments/Makefile +++ b/ee/ephemeral_environments/Makefile @@ -23,6 +23,9 @@ ifdef CI sem-service start postgres 9.6 --db=$(POSTGRES_DB_NAME) --user=$(POSTGRES_DB_USER) --password=$(POSTGRES_DB_PASSWORD) endif +console.ex: + docker compose run --service-ports $(CONTAINER_ENV_VARS) --rm app sh -c "mix ecto.create && mix ecto.migrate && iex -S mix" + migration.gen: @if [ -z "$(NAME)" ]; then \ echo "Usage: make migration.gen NAME={migration_name}"; \ diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/application.ex b/ee/ephemeral_environments/lib/ephemeral_environments/application.ex index 5fa2848ae..102d635a9 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/application.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/application.ex @@ -9,7 +9,16 @@ defmodule EphemeralEnvironments.Application do "Starting EphemeralEnvironments in '#{Application.get_env(:ephemeral_environments, :env)}' environment" ) - children = [EphemeralEnvironments.Repo] + grpc_port = Application.get_env(:ephemeral_environments, :grpc_listen_port) + + children = [ + EphemeralEnvironments.Repo, + {GRPC.Server.Supervisor, + servers: [EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer], + port: Application.get_env(:ephemeral_environments, :grpc_listen_port), + start_server: true, + adapter_opts: [ip: {0, 0, 0, 0}]} + ] opts = [strategy: :one_for_one, name: EphemeralEnvironments.Supervisor] Enum.each(children, fn child -> Logger.info("Starting #{inspect(child)}") end) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex new file mode 100644 index 000000000..473024299 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex @@ -0,0 +1,30 @@ +defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do + use Ecto.Schema + import Ecto.Changeset + + @primary_key {:id, :binary_id, autogenerate: true} + @foreign_key_type :binary_id + + schema "ephemeral_environment_types" do + field :org_id, :binary_id + field :name, :string + field :description, :string + field :created_by, :binary_id + field :last_modified_by, :binary_id + field :state, :string + field :max_number_of_instances, :integer + + timestamps() + end + + @doc false + def changeset(ephemeral_environment_type, attrs) do + ephemeral_environment_type + |> cast(attrs, [:org_id, :name, :description, :created_by, :last_modified_by, :state, :max_number_of_instances]) + |> validate_required([:org_id, :name, :created_by, :last_modified_by, :state]) + |> validate_length(:name, min: 1, max: 255) + |> validate_length(:description, max: 1000) + |> validate_inclusion(:state, [:draft, :ready, :cordoned, :deleted]) + |> validate_number(:max_number_of_instances, greater_than: 0) + end +end \ No newline at end of file diff --git a/ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex new file mode 100644 index 000000000..8862453db --- /dev/null +++ b/ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex @@ -0,0 +1,42 @@ +defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do + use GRPC.Server, service: InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service + + alias InternalApi.EphemeralEnvironments.{ + ListRequest, + ListResponse, + DescribeRequest, + DescribeResponse, + CreateRequest, + CreateResponse, + UpdateRequest, + UpdateResponse, + DeleteRequest, + DeleteResponse, + CordonRequest, + CordonResponse + } + + def list(_request, _stream) do + %ListResponse{} + end + + def describe(_request, _stream) do + %DescribeResponse{} + end + + def create(_request, _stream) do + %CreateResponse{} + end + + def update(_request, _stream) do + %UpdateResponse{} + end + + def delete(_request, _stream) do + %DeleteResponse{} + end + + def cordon(_request, _stream) do + %CordonResponse{} + end +end \ No newline at end of file From 10f0d4fbb72f78d8031eaf21d73d956965af93ff Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 2 Oct 2025 10:39:04 +0200 Subject: [PATCH 03/21] Explicitly set mapped port in docker-compose --- ee/ephemeral_environments/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/ephemeral_environments/docker-compose.yml b/ee/ephemeral_environments/docker-compose.yml index fc0dd0bab..5a5da0556 100644 --- a/ee/ephemeral_environments/docker-compose.yml +++ b/ee/ephemeral_environments/docker-compose.yml @@ -11,7 +11,7 @@ services: args: - BUILD_ENV=test ports: - - "50051" + - 60051:50051 env_file: - .env volumes: @@ -41,4 +41,4 @@ services: volumes: postgres-data: - driver: local \ No newline at end of file + driver: local From 721d3d17bb0183e4abef0c322e26a3b2077b1357 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 2 Oct 2025 10:40:03 +0200 Subject: [PATCH 04/21] Add endpoint and intercepters --- .../lib/ephemeral_environments/application.ex | 9 +++------ .../lib/ephemeral_environments/grpc/endpoint.ex | 10 ++++++++++ .../grpc/ephemeral_environments_server.ex | 5 +++-- .../grpc/interceptors/metrics.ex | 13 +++++++++++++ .../grpc/interceptors/proto_converte.ex | 14 ++++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 ee/ephemeral_environments/lib/ephemeral_environments/grpc/endpoint.ex rename ee/ephemeral_environments/lib/{ => ephemeral_environments}/grpc/ephemeral_environments_server.ex (85%) create mode 100644 ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/metrics.ex create mode 100644 ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/application.ex b/ee/ephemeral_environments/lib/ephemeral_environments/application.ex index 102d635a9..18f947b2a 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/application.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/application.ex @@ -9,15 +9,12 @@ defmodule EphemeralEnvironments.Application do "Starting EphemeralEnvironments in '#{Application.get_env(:ephemeral_environments, :env)}' environment" ) - grpc_port = Application.get_env(:ephemeral_environments, :grpc_listen_port) - children = [ EphemeralEnvironments.Repo, {GRPC.Server.Supervisor, - servers: [EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer], - port: Application.get_env(:ephemeral_environments, :grpc_listen_port), - start_server: true, - adapter_opts: [ip: {0, 0, 0, 0}]} + endpoint: EphemeralEnvironments.Grpc.Endpoint, + port: Application.fetch_env!(:ephemeral_environments, :grpc_listen_port), + start_server: true} ] opts = [strategy: :one_for_one, name: EphemeralEnvironments.Supervisor] diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/endpoint.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/endpoint.ex new file mode 100644 index 000000000..f896177e3 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/endpoint.ex @@ -0,0 +1,10 @@ +defmodule EphemeralEnvironments.Grpc.Endpoint do + use GRPC.Endpoint + + run(EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer, + interceptors: [ + EphemeralEnvironments.Grpc.Interceptor.Metrics, + EphemeralEnvironments.Grpc.Interceptor.ProtoConverter + ] + ) +end diff --git a/ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex similarity index 85% rename from ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex rename to ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index 8862453db..cb16739a6 100644 --- a/ee/ephemeral_environments/lib/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -24,7 +24,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do %DescribeResponse{} end - def create(_request, _stream) do + def create(request, _stream) do + ret = EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) %CreateResponse{} end @@ -39,4 +40,4 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do def cordon(_request, _stream) do %CordonResponse{} end -end \ No newline at end of file +end diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/metrics.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/metrics.ex new file mode 100644 index 000000000..3456ed1c1 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/metrics.ex @@ -0,0 +1,13 @@ +defmodule EphemeralEnvironments.Grpc.Interceptor.Metrics do + @behaviour GRPC.ServerInterceptor + require Logger + + def init(options) do + options + end + + def call(request, stream, next, _) do + Logger.debug("Metrics intercepter #{inspect(request)}") + next.(request, stream) + end +end diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex new file mode 100644 index 000000000..4793c7050 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex @@ -0,0 +1,14 @@ +defmodule EphemeralEnvironments.Grpc.Interceptor.ProtoConverter do + @behaviour GRPC.ServerInterceptor + require Logger + + def init(options) do + options + end + + def call(request, stream, next, _) do + Logger.debug("Proto intercepter #{inspect(request)}") + converted = EphemeralEnvironments.Utils.Proto.to_map(request) + next.(converted, stream) + end +end From 16415e754bbccdc5e9ca6a9f41a7580f84faeff2 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 2 Oct 2025 10:40:27 +0200 Subject: [PATCH 05/21] Generated protobufs --- .../internal_api/ephemeral_environments.pb.ex | 261 ++++++++++-------- .../include/google/protobuf/timestamp.pb.ex | 6 +- 2 files changed, 146 insertions(+), 121 deletions(-) diff --git a/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex index ce170d882..0b24f5618 100644 --- a/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex +++ b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex @@ -3,22 +3,22 @@ defmodule InternalApi.EphemeralEnvironments.InstanceState do use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :INSTANCE_STATE_UNSPECIFIED, 0 - field :INSTANCE_STATE_ZERO_STATE, 1 - field :INSTANCE_STATE_PROVISIONING, 2 - field :INSTANCE_STATE_READY_TO_USE, 3 - field :INSTANCE_STATE_SLEEP, 4 - field :INSTANCE_STATE_IN_USE, 5 - field :INSTANCE_STATE_DEPLOYING, 6 - field :INSTANCE_STATE_DEPROVISIONING, 7 - field :INSTANCE_STATE_DESTROYED, 8 - field :INSTANCE_STATE_ACKNOWLEDGED_CLEANUP, 9 - field :INSTANCE_STATE_FAILED_PROVISIONING, 10 - field :INSTANCE_STATE_FAILED_DEPROVISIONING, 11 - field :INSTANCE_STATE_FAILED_DEPLOYMENT, 12 - field :INSTANCE_STATE_FAILED_CLEANUP, 13 - field :INSTANCE_STATE_FAILED_SLEEP, 14 - field :INSTANCE_STATE_FAILED_WAKE_UP, 15 + field(:INSTANCE_STATE_UNSPECIFIED, 0) + field(:INSTANCE_STATE_ZERO_STATE, 1) + field(:INSTANCE_STATE_PROVISIONING, 2) + field(:INSTANCE_STATE_READY_TO_USE, 3) + field(:INSTANCE_STATE_SLEEP, 4) + field(:INSTANCE_STATE_IN_USE, 5) + field(:INSTANCE_STATE_DEPLOYING, 6) + field(:INSTANCE_STATE_DEPROVISIONING, 7) + field(:INSTANCE_STATE_DESTROYED, 8) + field(:INSTANCE_STATE_ACKNOWLEDGED_CLEANUP, 9) + field(:INSTANCE_STATE_FAILED_PROVISIONING, 10) + field(:INSTANCE_STATE_FAILED_DEPROVISIONING, 11) + field(:INSTANCE_STATE_FAILED_DEPLOYMENT, 12) + field(:INSTANCE_STATE_FAILED_CLEANUP, 13) + field(:INSTANCE_STATE_FAILED_SLEEP, 14) + field(:INSTANCE_STATE_FAILED_WAKE_UP, 15) end defmodule InternalApi.EphemeralEnvironments.StateChangeActionType do @@ -26,14 +26,14 @@ defmodule InternalApi.EphemeralEnvironments.StateChangeActionType do use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :STATE_CHANGE_ACTION_TYPE_UNSPECIFIED, 0 - field :STATE_CHANGE_ACTION_TYPE_PROVISIONING, 1 - field :STATE_CHANGE_ACTION_TYPE_CLEANUP, 2 - field :STATE_CHANGE_ACTION_TYPE_TO_SLEEP, 3 - field :STATE_CHANGE_ACTION_TYPE_WAKE_UP, 4 - field :STATE_CHANGE_ACTION_TYPE_DEPLOYING, 5 - field :STATE_CHANGE_ACTION_TYPE_CLEANING_UP, 6 - field :STATE_CHANGE_ACTION_TYPE_DEPROVISIONING, 7 + field(:STATE_CHANGE_ACTION_TYPE_UNSPECIFIED, 0) + field(:STATE_CHANGE_ACTION_TYPE_PROVISIONING, 1) + field(:STATE_CHANGE_ACTION_TYPE_CLEANUP, 2) + field(:STATE_CHANGE_ACTION_TYPE_TO_SLEEP, 3) + field(:STATE_CHANGE_ACTION_TYPE_WAKE_UP, 4) + field(:STATE_CHANGE_ACTION_TYPE_DEPLOYING, 5) + field(:STATE_CHANGE_ACTION_TYPE_CLEANING_UP, 6) + field(:STATE_CHANGE_ACTION_TYPE_DEPROVISIONING, 7) end defmodule InternalApi.EphemeralEnvironments.StateChangeResult do @@ -41,10 +41,10 @@ defmodule InternalApi.EphemeralEnvironments.StateChangeResult do use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :STATE_CHANGE_RESULT_UNSPECIFIED, 0 - field :STATE_CHANGE_RESULT_PASSED, 1 - field :STATE_CHANGE_RESULT_PENDING, 2 - field :STATE_CHANGE_RESULT_FAILED, 3 + field(:STATE_CHANGE_RESULT_UNSPECIFIED, 0) + field(:STATE_CHANGE_RESULT_PASSED, 1) + field(:STATE_CHANGE_RESULT_PENDING, 2) + field(:STATE_CHANGE_RESULT_FAILED, 3) end defmodule InternalApi.EphemeralEnvironments.TriggererType do @@ -52,9 +52,9 @@ defmodule InternalApi.EphemeralEnvironments.TriggererType do use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :TRIGGERER_TYPE_UNSPECIFIED, 0 - field :TRIGGERER_TYPE_USER, 1 - field :TRIGGERER_TYPE_AUTOMATION_RULE, 2 + field(:TRIGGERER_TYPE_UNSPECIFIED, 0) + field(:TRIGGERER_TYPE_USER, 1) + field(:TRIGGERER_TYPE_AUTOMATION_RULE, 2) end defmodule InternalApi.EphemeralEnvironments.TypeState do @@ -62,11 +62,11 @@ defmodule InternalApi.EphemeralEnvironments.TypeState do use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :TYPE_STATE_UNSPECIFIED, 0 - field :TYPE_STATE_DRAFT, 1 - field :TYPE_STATE_READY, 2 - field :TYPE_STATE_CORDONED, 3 - field :TYPE_STATE_DELETED, 4 + field(:TYPE_STATE_UNSPECIFIED, 0) + field(:TYPE_STATE_DRAFT, 1) + field(:TYPE_STATE_READY, 2) + field(:TYPE_STATE_CORDONED, 3) + field(:TYPE_STATE_DELETED, 4) end defmodule InternalApi.EphemeralEnvironments.ListRequest do @@ -74,8 +74,8 @@ defmodule InternalApi.EphemeralEnvironments.ListRequest do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :org_id, 1, type: :string, json_name: "orgId" - field :project_id, 2, type: :string, json_name: "projectId" + field(:org_id, 1, type: :string, json_name: "orgId") + field(:project_id, 2, type: :string, json_name: "projectId") end defmodule InternalApi.EphemeralEnvironments.ListResponse do @@ -83,10 +83,11 @@ defmodule InternalApi.EphemeralEnvironments.ListResponse do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_types, 1, + field(:environment_types, 1, repeated: true, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentTypes" + ) end defmodule InternalApi.EphemeralEnvironments.DescribeRequest do @@ -94,8 +95,8 @@ defmodule InternalApi.EphemeralEnvironments.DescribeRequest do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :org_id, 2, type: :string, json_name: "orgId" + field(:id, 1, type: :string) + field(:org_id, 2, type: :string, json_name: "orgId") end defmodule InternalApi.EphemeralEnvironments.DescribeResponse do @@ -103,13 +104,15 @@ defmodule InternalApi.EphemeralEnvironments.DescribeResponse do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_type, 1, + field(:environment_type, 1, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentType" + ) - field :instances, 2, + field(:instances, 2, repeated: true, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentInstance + ) end defmodule InternalApi.EphemeralEnvironments.CreateRequest do @@ -117,9 +120,10 @@ defmodule InternalApi.EphemeralEnvironments.CreateRequest do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_type, 1, + field(:environment_type, 1, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentType" + ) end defmodule InternalApi.EphemeralEnvironments.CreateResponse do @@ -127,9 +131,10 @@ defmodule InternalApi.EphemeralEnvironments.CreateResponse do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_type, 1, + field(:environment_type, 1, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentType" + ) end defmodule InternalApi.EphemeralEnvironments.UpdateRequest do @@ -137,9 +142,10 @@ defmodule InternalApi.EphemeralEnvironments.UpdateRequest do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_type, 1, + field(:environment_type, 1, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentType" + ) end defmodule InternalApi.EphemeralEnvironments.UpdateResponse do @@ -147,9 +153,10 @@ defmodule InternalApi.EphemeralEnvironments.UpdateResponse do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_type, 1, + field(:environment_type, 1, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentType" + ) end defmodule InternalApi.EphemeralEnvironments.DeleteRequest do @@ -157,8 +164,8 @@ defmodule InternalApi.EphemeralEnvironments.DeleteRequest do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :org_id, 2, type: :string, json_name: "orgId" + field(:id, 1, type: :string) + field(:org_id, 2, type: :string, json_name: "orgId") end defmodule InternalApi.EphemeralEnvironments.DeleteResponse do @@ -172,8 +179,8 @@ defmodule InternalApi.EphemeralEnvironments.CordonRequest do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :org_id, 2, type: :string, json_name: "orgId" + field(:id, 1, type: :string) + field(:org_id, 2, type: :string, json_name: "orgId") end defmodule InternalApi.EphemeralEnvironments.CordonResponse do @@ -181,9 +188,10 @@ defmodule InternalApi.EphemeralEnvironments.CordonResponse do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :environment_type, 1, + field(:environment_type, 1, type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, json_name: "environmentType" + ) end defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentType do @@ -191,16 +199,16 @@ defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentType do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :org_id, 2, type: :string, json_name: "orgId" - field :name, 3, type: :string - field :description, 4, type: :string - field :created_by, 5, type: :string, json_name: "createdBy" - field :last_updated_by, 6, type: :string, json_name: "lastUpdatedBy" - field :created_at, 7, type: Google.Protobuf.Timestamp, json_name: "createdAt" - field :updated_at, 8, type: Google.Protobuf.Timestamp, json_name: "updatedAt" - field :state, 9, type: InternalApi.EphemeralEnvironments.TypeState, enum: true - field :max_number_of_instances, 10, type: :int32, json_name: "maxNumberOfInstances" + field(:id, 1, type: :string) + field(:org_id, 2, type: :string, json_name: "orgId") + field(:name, 3, type: :string) + field(:description, 4, type: :string) + field(:created_by, 5, type: :string, json_name: "createdBy") + field(:last_updated_by, 6, type: :string, json_name: "lastUpdatedBy") + field(:created_at, 7, type: Google.Protobuf.Timestamp, json_name: "createdAt") + field(:updated_at, 8, type: Google.Protobuf.Timestamp, json_name: "updatedAt") + field(:state, 9, type: InternalApi.EphemeralEnvironments.TypeState, enum: true) + field(:max_number_of_instances, 10, type: :int32, json_name: "maxNumberOfInstances") end defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentInstance do @@ -208,13 +216,13 @@ defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentInstance do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :ee_type_id, 2, type: :string, json_name: "eeTypeId" - field :name, 3, type: :string - field :state, 4, type: InternalApi.EphemeralEnvironments.InstanceState, enum: true - field :last_state_change_id, 5, type: :string, json_name: "lastStateChangeId" - field :created_at, 6, type: Google.Protobuf.Timestamp, json_name: "createdAt" - field :updated_at, 7, type: Google.Protobuf.Timestamp, json_name: "updatedAt" + field(:id, 1, type: :string) + field(:ee_type_id, 2, type: :string, json_name: "eeTypeId") + field(:name, 3, type: :string) + field(:state, 4, type: InternalApi.EphemeralEnvironments.InstanceState, enum: true) + field(:last_state_change_id, 5, type: :string, json_name: "lastStateChangeId") + field(:created_at, 6, type: Google.Protobuf.Timestamp, json_name: "createdAt") + field(:updated_at, 7, type: Google.Protobuf.Timestamp, json_name: "updatedAt") end defmodule InternalApi.EphemeralEnvironments.EphemeralSecretDefinition do @@ -222,20 +230,22 @@ defmodule InternalApi.EphemeralEnvironments.EphemeralSecretDefinition do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :ee_type_id, 2, type: :string, json_name: "eeTypeId" - field :name, 3, type: :string - field :description, 4, type: :string + field(:id, 1, type: :string) + field(:ee_type_id, 2, type: :string, json_name: "eeTypeId") + field(:name, 3, type: :string) + field(:description, 4, type: :string) - field :actions_that_can_change_the_secret, 5, + field(:actions_that_can_change_the_secret, 5, repeated: true, type: InternalApi.EphemeralEnvironments.StateChangeAction, json_name: "actionsThatCanChangeTheSecret" + ) - field :actions_that_have_access_to_the_secret, 6, + field(:actions_that_have_access_to_the_secret, 6, repeated: true, type: InternalApi.EphemeralEnvironments.StateChangeAction, json_name: "actionsThatHaveAccessToTheSecret" + ) end defmodule InternalApi.EphemeralEnvironments.StateChangeAction do @@ -243,12 +253,12 @@ defmodule InternalApi.EphemeralEnvironments.StateChangeAction do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :type, 2, type: InternalApi.EphemeralEnvironments.StateChangeActionType, enum: true - field :project_id, 3, type: :string, json_name: "projectId" - field :branch, 4, type: :string - field :pipeline_yaml_name, 5, type: :string, json_name: "pipelineYamlName" - field :execution_auth_rules, 6, type: :string, json_name: "executionAuthRules" + field(:id, 1, type: :string) + field(:type, 2, type: InternalApi.EphemeralEnvironments.StateChangeActionType, enum: true) + field(:project_id, 3, type: :string, json_name: "projectId") + field(:branch, 4, type: :string) + field(:pipeline_yaml_name, 5, type: :string, json_name: "pipelineYamlName") + field(:execution_auth_rules, 6, type: :string, json_name: "executionAuthRules") end defmodule InternalApi.EphemeralEnvironments.InstanceStateChange do @@ -256,30 +266,33 @@ defmodule InternalApi.EphemeralEnvironments.InstanceStateChange do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :id, 1, type: :string - field :instance_id, 2, type: :string, json_name: "instanceId" + field(:id, 1, type: :string) + field(:instance_id, 2, type: :string, json_name: "instanceId") - field :prev_state, 3, + field(:prev_state, 3, type: InternalApi.EphemeralEnvironments.InstanceState, json_name: "prevState", enum: true + ) - field :next_state, 4, + field(:next_state, 4, type: InternalApi.EphemeralEnvironments.InstanceState, json_name: "nextState", enum: true + ) - field :state_change_action, 5, + field(:state_change_action, 5, type: InternalApi.EphemeralEnvironments.StateChangeAction, json_name: "stateChangeAction" - - field :result, 6, type: InternalApi.EphemeralEnvironments.StateChangeResult, enum: true - field :TriggererType, 7, type: :string - field :trigger_id, 8, type: :string, json_name: "triggerId" - field :execution_pipeline_id, 9, type: :string, json_name: "executionPipelineId" - field :execution_workflow_id, 10, type: :string, json_name: "executionWorkflowId" - field :started_at, 11, type: Google.Protobuf.Timestamp, json_name: "startedAt" - field :finished_at, 12, type: Google.Protobuf.Timestamp, json_name: "finishedAt" + ) + + field(:result, 6, type: InternalApi.EphemeralEnvironments.StateChangeResult, enum: true) + field(:TriggererType, 7, type: :string) + field(:trigger_id, 8, type: :string, json_name: "triggerId") + field(:execution_pipeline_id, 9, type: :string, json_name: "executionPipelineId") + field(:execution_workflow_id, 10, type: :string, json_name: "executionWorkflowId") + field(:started_at, 11, type: Google.Protobuf.Timestamp, json_name: "startedAt") + field(:finished_at, 12, type: Google.Protobuf.Timestamp, json_name: "finishedAt") end defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service do @@ -289,33 +302,45 @@ defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service do name: "InternalApi.EphemeralEnvironments.EphemeralEnvironments", protoc_gen_elixir_version: "0.13.0" - rpc :List, - InternalApi.EphemeralEnvironments.ListRequest, - InternalApi.EphemeralEnvironments.ListResponse - - rpc :Describe, - InternalApi.EphemeralEnvironments.DescribeRequest, - InternalApi.EphemeralEnvironments.DescribeResponse - - rpc :Create, - InternalApi.EphemeralEnvironments.CreateRequest, - InternalApi.EphemeralEnvironments.CreateResponse - - rpc :Update, - InternalApi.EphemeralEnvironments.UpdateRequest, - InternalApi.EphemeralEnvironments.UpdateResponse - - rpc :Delete, - InternalApi.EphemeralEnvironments.DeleteRequest, - InternalApi.EphemeralEnvironments.DeleteResponse - - rpc :Cordon, - InternalApi.EphemeralEnvironments.CordonRequest, - InternalApi.EphemeralEnvironments.CordonResponse + rpc( + :List, + InternalApi.EphemeralEnvironments.ListRequest, + InternalApi.EphemeralEnvironments.ListResponse + ) + + rpc( + :Describe, + InternalApi.EphemeralEnvironments.DescribeRequest, + InternalApi.EphemeralEnvironments.DescribeResponse + ) + + rpc( + :Create, + InternalApi.EphemeralEnvironments.CreateRequest, + InternalApi.EphemeralEnvironments.CreateResponse + ) + + rpc( + :Update, + InternalApi.EphemeralEnvironments.UpdateRequest, + InternalApi.EphemeralEnvironments.UpdateResponse + ) + + rpc( + :Delete, + InternalApi.EphemeralEnvironments.DeleteRequest, + InternalApi.EphemeralEnvironments.DeleteResponse + ) + + rpc( + :Cordon, + InternalApi.EphemeralEnvironments.CordonRequest, + InternalApi.EphemeralEnvironments.CordonResponse + ) end defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironments.Stub do @moduledoc false use GRPC.Stub, service: InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service -end \ No newline at end of file +end diff --git a/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex b/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex index d7e0566d9..410019fb7 100644 --- a/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex +++ b/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex @@ -3,6 +3,6 @@ defmodule Google.Protobuf.Timestamp do use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field :seconds, 1, type: :int64 - field :nanos, 2, type: :int32 -end \ No newline at end of file + field(:seconds, 1, type: :int64) + field(:nanos, 2, type: :int32) +end From e34c50bdf53f6c2d9aecf5ad71486efcf6483ade Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 2 Oct 2025 10:40:47 +0200 Subject: [PATCH 06/21] Util from converting protos to map --- .../lib/ephemeral_environments/utils/proto.ex | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex new file mode 100644 index 000000000..48583c7ab --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex @@ -0,0 +1,119 @@ +defmodule EphemeralEnvironments.Utils.Proto do + @moduledoc """ + Utility functions for converting protobuf structs to plain Elixir maps. + """ + + @doc """ + Recursively converts a protobuf struct to a plain Elixir map. + - Converts Google.Protobuf.Timestamp to DateTime + - Converts enums to their atom names (INSTANCE_STATE_PROVISIONING -> :provisioning) + - Recursively processes nested structs + """ + def to_map(nil), do: nil + + def to_map(%Google.Protobuf.Timestamp{} = timestamp) do + DateTime.from_unix!(timestamp.seconds, :second) + |> DateTime.add(timestamp.nanos, :nanosecond) + end + + def to_map(%module{} = struct) when is_atom(module) do + struct + |> Map.from_struct() + |> Enum.map(fn {key, value} -> {key, convert_value(value, module, key)} end) + |> Map.new() + end + + def to_map(value), do: value + + defp convert_value(value, module, field) when is_list(value) do + Enum.map(value, &to_map/1) + end + + defp convert_value(value, module, field) when is_struct(value) do + to_map(value) + end + + defp convert_value(value, module, field) when is_integer(value) do + # Check if this field is an enum by looking at the field definition + case get_enum_module(module, field) do + nil -> value + enum_module -> integer_to_atom(enum_module, value) + end + end + + defp convert_value(value, module, field) when is_atom(value) do + # Check if this is an enum atom that needs normalization + case get_enum_module(module, field) do + nil -> value + enum_module -> normalize_enum_name(value, enum_module) + end + end + + defp convert_value(value, _module, _field), do: value + + # If given field is of type enum inside the parend module, the name of the enum module + # will be returned. Otherwise it will return nil. + defp get_enum_module(module, field) do + try do + field_props = module.__message_props__().field_props + + # Find the field by name_atom + field_info = + field_props + |> Enum.find(fn {_num, props} -> props.name_atom == field end) + |> case do + {_num, props} -> props + nil -> nil + end + + if field_info && field_info.enum? do + case field_info.type do + {:enum, enum_module} -> enum_module + _ -> nil + end + else + nil + end + rescue + _ -> nil + end + end + + defp integer_to_atom(enum_module, value) do + try do + enum_module.__message_props__() + |> Map.get(:field_props, %{}) + |> Enum.find(fn {_name, props} -> props[:enum_value] == value end) + |> case do + {name, _} -> normalize_enum_name(name, enum_module) + nil -> value + end + rescue + _ -> value + end + end + + # Normalize enum names by removing prefix and lowercasing + # E.g., :INSTANCE_STATE_ZERO_STATE -> :zero_state (for InternalApi.EphemeralEnvironments.InstanceState) + # :TYPE_STATE_DRAFT -> :draft (for InternalApi.EphemeralEnvironments.TypeState) + defp normalize_enum_name(enum_atom, enum_module) do + prefix = extract_enum_prefix(enum_module) + + enum_atom + |> Atom.to_string() + |> String.replace_prefix(prefix <> "_", "") + |> String.downcase() + |> String.to_atom() + end + + # Extract the enum prefix from the module name + # E.g., InternalApi.EphemeralEnvironments.InstanceState -> "INSTANCE_STATE" + # InternalApi.EphemeralEnvironments.StateChangeActionType -> "STATE_CHANGE_ACTION_TYPE" + defp extract_enum_prefix(enum_module) do + enum_module + |> Module.split() + |> List.last() + |> Macro.underscore() + |> String.upcase() + end +end From 6493f5d2528a995bf919c190072dc3d038c60e83 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 2 Oct 2025 10:41:42 +0200 Subject: [PATCH 07/21] Add service for inserting new types --- .../repo/ephemeral_environment_type.ex | 39 +++++++++++----- .../service/ephemeral_environment_type.ex | 45 +++++++++++++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex index 473024299..ea0a48a6e 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex @@ -6,13 +6,13 @@ defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do @foreign_key_type :binary_id schema "ephemeral_environment_types" do - field :org_id, :binary_id - field :name, :string - field :description, :string - field :created_by, :binary_id - field :last_modified_by, :binary_id - field :state, :string - field :max_number_of_instances, :integer + field(:org_id, :binary_id) + field(:name, :string) + field(:description, :string) + field(:created_by, :binary_id) + field(:last_modified_by, :binary_id) + field(:state, Ecto.Enum, values: [:draft, :ready, :cordoned, :deleted]) + field(:max_number_of_instances, :integer) timestamps() end @@ -20,11 +20,30 @@ defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do @doc false def changeset(ephemeral_environment_type, attrs) do ephemeral_environment_type - |> cast(attrs, [:org_id, :name, :description, :created_by, :last_modified_by, :state, :max_number_of_instances]) + |> cast(attrs, [ + :org_id, + :name, + :description, + :created_by, + :last_modified_by, + :state, + :max_number_of_instances + ]) |> validate_required([:org_id, :name, :created_by, :last_modified_by, :state]) + |> validate_uuid(:org_id) + |> validate_uuid(:created_by) + |> validate_uuid(:last_modified_by) |> validate_length(:name, min: 1, max: 255) |> validate_length(:description, max: 1000) - |> validate_inclusion(:state, [:draft, :ready, :cordoned, :deleted]) |> validate_number(:max_number_of_instances, greater_than: 0) end -end \ No newline at end of file + + defp validate_uuid(changeset, field) do + validate_change(changeset, field, fn ^field, value -> + case Ecto.UUID.cast(value) do + {:ok, _} -> [] + :error -> [{field, "must be a valid UUID"}] + end + end) + end +end diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex new file mode 100644 index 000000000..10c9f3593 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -0,0 +1,45 @@ +defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do + alias EphemeralEnvironments.Repo + alias EphemeralEnvironments.Repo.EphemeralEnvironmentType, as: Schema + + @doc """ + Creates a new ephemeral environment type. + + ## Parameters + - attrs: Map with keys: + - org_id (required) + - name (required) + - created_by (required) + - state (optional, defaults to :draft) + - description (optional) + - max_number_of_instances (optional) + + ## Returns + - {:ok, %Schema{}} on success + - {:error, String.t()} on validation failure + """ + def create(attrs) do + attrs = Map.put(attrs, :last_modified_by, attrs[:created_by]) + attrs = Map.put_new(attrs, :state, :draft) + + %Schema{} + |> Schema.changeset(attrs) + |> Repo.insert() + |> case do + {:ok, record} -> {:ok, record} + {:error, changeset} -> {:error, format_errors(changeset)} + end + end + + defp format_errors(changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", to_string(value)) + end) + end) + |> Enum.map(fn {field, errors} -> + "#{field}: #{Enum.join(errors, ", ")}" + end) + |> Enum.join("; ") + end +end From e5a3f30ba2abb55307fd3259d66d4edddb6a5bf2 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 2 Oct 2025 13:48:21 +0200 Subject: [PATCH 08/21] Create elixir map to proto converter --- .../grpc/ephemeral_environments_server.ex | 5 +- .../repo/ephemeral_environment_type.ex | 1 - .../service/ephemeral_environment_type.ex | 14 ++- .../lib/ephemeral_environments/utils/proto.ex | 107 +++++++++++++++++- 4 files changed, 121 insertions(+), 6 deletions(-) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index cb16739a6..4ee39a3cc 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -25,8 +25,9 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end def create(request, _stream) do - ret = EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) - %CreateResponse{} + {:ok, ret} = EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) + converted = EphemeralEnvironments.Utils.Proto.from_map(%{environment_type: ret}, CreateResponse) + converted end def update(_request, _stream) do diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex index ea0a48a6e..ac594e394 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex @@ -17,7 +17,6 @@ defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do timestamps() end - @doc false def changeset(ephemeral_environment_type, attrs) do ephemeral_environment_type |> cast(attrs, [ diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 10c9f3593..5777714f1 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -15,7 +15,7 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do - max_number_of_instances (optional) ## Returns - - {:ok, %Schema{}} on success + - {:ok, map} on success - {:error, String.t()} on validation failure """ def create(attrs) do @@ -26,11 +26,21 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do |> Schema.changeset(attrs) |> Repo.insert() |> case do - {:ok, record} -> {:ok, record} + {:ok, record} -> {:ok, struct_to_map(record)} {:error, changeset} -> {:error, format_errors(changeset)} end end + ### + ### Helper functions + ### + + defp struct_to_map(struct) do + struct + |> Map.from_struct() + |> Map.drop([:__meta__]) + end + defp format_errors(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Enum.reduce(opts, msg, fn {key, value}, acc -> diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex index 48583c7ab..4f034955f 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex @@ -1,8 +1,37 @@ defmodule EphemeralEnvironments.Utils.Proto do @moduledoc """ - Utility functions for converting protobuf structs to plain Elixir maps. + Utility functions for converting between protobuf structs and plain Elixir maps. """ + @doc """ + Converts an Elixir map to a protobuf struct of the given module type. + - Converts DateTime to Google.Protobuf.Timestamp + - Converts normalized enum atoms (:ready) to protobuf enum atoms (:TYPE_STATE_READY) + - Recursively processes nested maps to nested proto structs + + ## Examples + + from_map(%{name: "test", state: :ready}, InternalApi.EphemeralEnvironments.EphemeralEnvironmentType) + """ + def from_map(nil, _module), do: nil + + def from_map(map, module) when is_map(map) and is_atom(module) do + field_props = module.__message_props__().field_props + + # Convert map to struct fields + fields = + map + |> Enum.map(fn {key, value} -> + # Find field info for this key + field_info = find_field_info(field_props, key) + converted_value = convert_value_from_map(value, field_info) + {key, converted_value} + end) + |> Enum.into(%{}) + + struct(module, fields) + end + @doc """ Recursively converts a protobuf struct to a plain Elixir map. - Converts Google.Protobuf.Timestamp to DateTime @@ -116,4 +145,80 @@ defmodule EphemeralEnvironments.Utils.Proto do |> Macro.underscore() |> String.upcase() end + + # Find field info by field name atom + defp find_field_info(field_props, field_name) do + field_props + |> Enum.find(fn {_num, props} -> props.name_atom == field_name end) + |> case do + {_num, props} -> props + nil -> nil + end + end + + defp convert_value_from_map(nil, _field_info), do: nil + + defp convert_value_from_map(%DateTime{} = dt, _field_info) do + %Google.Protobuf.Timestamp{ + seconds: DateTime.to_unix(dt), + nanos: 0 + } + end + + @unix_epoch ~N[1970-01-01 00:00:00] + defp convert_value_from_map(%NaiveDateTime{} = ndt, _field_info) do + %Google.Protobuf.Timestamp{ + seconds: NaiveDateTime.diff(ndt, @unix_epoch) + } + end + + defp convert_value_from_map(value, nil), do: value + + defp convert_value_from_map(values, field_info) when is_list(values) do + if field_info.embedded? do + Enum.map(values, fn item -> + if is_map(item) and not is_struct(item) do + from_map(item, field_info.type) + else + item + end + end) + else + values + end + end + + # Handle nested maps (embedded messages) + defp convert_value_from_map(value, field_info) when is_map(value) and not is_struct(value) do + if field_info.embedded? do + from_map(value, field_info.type) + else + value + end + end + + # Handle enum atoms - convert normalized atom back to proto enum + defp convert_value_from_map(value, field_info) when is_atom(value) do + if field_info.enum? do + case field_info.type do + {:enum, enum_module} -> denormalize_enum_name(value, enum_module) + _ -> value + end + else + value + end + end + + defp convert_value_from_map(value, _field_info), do: value + + # Denormalize enum: :ready -> :TYPE_STATE_READY + defp denormalize_enum_name(normalized_atom, enum_module) do + prefix = extract_enum_prefix(enum_module) + + normalized_atom + |> Atom.to_string() + |> String.upcase() + |> then(&"#{prefix}_#{&1}") + |> String.to_atom() + end end From 59845a23967a85f99d8d2b785d309688b75882e6 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 16:11:23 +0200 Subject: [PATCH 09/21] Generate new protobufs --- .../internal_api/ephemeral_environments.pb.ex | 131 ++++- .../lib/internal_api/rbac.pb.ex | 464 ++++++++++++++++++ .../scripts/internal_protos.sh | 1 + 3 files changed, 579 insertions(+), 17 deletions(-) create mode 100644 ee/ephemeral_environments/lib/internal_api/rbac.pb.ex diff --git a/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex index 0b24f5618..9287abca7 100644 --- a/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex +++ b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex @@ -1,3 +1,14 @@ +defmodule InternalApi.EphemeralEnvironments.StageType do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:STAGE_TYPE_UNSPECIFIED, 0) + field(:STAGE_TYPE_PROVISION, 1) + field(:STAGE_TYPE_DEPLOY, 2) + field(:STAGE_TYPE_DEPROVISION, 3) +end + defmodule InternalApi.EphemeralEnvironments.InstanceState do @moduledoc false @@ -137,29 +148,22 @@ defmodule InternalApi.EphemeralEnvironments.CreateResponse do ) end -defmodule InternalApi.EphemeralEnvironments.UpdateRequest do +defmodule InternalApi.EphemeralEnvironments.DeleteRequest do @moduledoc false use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field(:environment_type, 1, - type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, - json_name: "environmentType" - ) + field(:id, 1, type: :string) + field(:org_id, 2, type: :string, json_name: "orgId") end -defmodule InternalApi.EphemeralEnvironments.UpdateResponse do +defmodule InternalApi.EphemeralEnvironments.DeleteResponse do @moduledoc false use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - - field(:environment_type, 1, - type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, - json_name: "environmentType" - ) end -defmodule InternalApi.EphemeralEnvironments.DeleteRequest do +defmodule InternalApi.EphemeralEnvironments.CordonRequest do @moduledoc false use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" @@ -168,22 +172,29 @@ defmodule InternalApi.EphemeralEnvironments.DeleteRequest do field(:org_id, 2, type: :string, json_name: "orgId") end -defmodule InternalApi.EphemeralEnvironments.DeleteResponse do +defmodule InternalApi.EphemeralEnvironments.CordonResponse do @moduledoc false use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" + ) end -defmodule InternalApi.EphemeralEnvironments.CordonRequest do +defmodule InternalApi.EphemeralEnvironments.UpdateRequest do @moduledoc false use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - field(:id, 1, type: :string) - field(:org_id, 2, type: :string, json_name: "orgId") + field(:environment_type, 1, + type: InternalApi.EphemeralEnvironments.EphemeralEnvironmentType, + json_name: "environmentType" + ) end -defmodule InternalApi.EphemeralEnvironments.CordonResponse do +defmodule InternalApi.EphemeralEnvironments.UpdateResponse do @moduledoc false use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" @@ -209,6 +220,92 @@ defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentType do field(:updated_at, 8, type: Google.Protobuf.Timestamp, json_name: "updatedAt") field(:state, 9, type: InternalApi.EphemeralEnvironments.TypeState, enum: true) field(:max_number_of_instances, 10, type: :int32, json_name: "maxNumberOfInstances") + field(:stages, 11, repeated: true, type: InternalApi.EphemeralEnvironments.StageConfig) + + field(:environment_context, 12, + repeated: true, + type: InternalApi.EphemeralEnvironments.EnvironmentContext, + json_name: "environmentContext" + ) + + field(:accessible_project_ids, 13, + repeated: true, + type: :string, + json_name: "accessibleProjectIds" + ) + + field(:ttl_config, 14, + type: InternalApi.EphemeralEnvironments.TTLConfig, + json_name: "ttlConfig" + ) +end + +defmodule InternalApi.EphemeralEnvironments.StageConfig do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:type, 1, type: InternalApi.EphemeralEnvironments.StageType, enum: true) + field(:pipeline, 2, type: InternalApi.EphemeralEnvironments.PipelineConfig) + field(:parameters, 3, repeated: true, type: InternalApi.EphemeralEnvironments.StageParameter) + + field(:rbac_rules, 4, + repeated: true, + type: InternalApi.EphemeralEnvironments.RBACRule, + json_name: "rbacRules" + ) +end + +defmodule InternalApi.EphemeralEnvironments.StageParameter do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:name, 1, type: :string) + field(:description, 2, type: :string) + field(:required, 3, type: :bool) +end + +defmodule InternalApi.EphemeralEnvironments.RBACRule do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:subject_type, 1, + type: InternalApi.RBAC.SubjectType, + json_name: "subjectType", + enum: true + ) + + field(:subject_id, 2, type: :string, json_name: "subjectId") +end + +defmodule InternalApi.EphemeralEnvironments.EnvironmentContext do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:name, 1, type: :string) + field(:description, 2, type: :string) +end + +defmodule InternalApi.EphemeralEnvironments.PipelineConfig do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:project_id, 1, type: :string, json_name: "projectId") + field(:branch, 2, type: :string) + field(:pipeline_yaml_file, 3, type: :string, json_name: "pipelineYamlFile") +end + +defmodule InternalApi.EphemeralEnvironments.TTLConfig do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:duration_hours, 1, type: :int32, json_name: "durationHours") + field(:allow_extension, 2, type: :bool, json_name: "allowExtension") end defmodule InternalApi.EphemeralEnvironments.EphemeralEnvironmentInstance do diff --git a/ee/ephemeral_environments/lib/internal_api/rbac.pb.ex b/ee/ephemeral_environments/lib/internal_api/rbac.pb.ex new file mode 100644 index 000000000..a0568caea --- /dev/null +++ b/ee/ephemeral_environments/lib/internal_api/rbac.pb.ex @@ -0,0 +1,464 @@ +defmodule InternalApi.RBAC.SubjectType do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:USER, 0) + field(:GROUP, 1) + field(:SERVICE_ACCOUNT, 2) +end + +defmodule InternalApi.RBAC.Scope do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:SCOPE_UNSPECIFIED, 0) + field(:SCOPE_ORG, 1) + field(:SCOPE_PROJECT, 2) +end + +defmodule InternalApi.RBAC.RoleBindingSource do + @moduledoc false + + use Protobuf, enum: true, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:ROLE_BINDING_SOURCE_UNSPECIFIED, 0) + field(:ROLE_BINDING_SOURCE_MANUALLY, 1) + field(:ROLE_BINDING_SOURCE_GITHUB, 2) + field(:ROLE_BINDING_SOURCE_BITBUCKET, 3) + field(:ROLE_BINDING_SOURCE_GITLAB, 4) + field(:ROLE_BINDING_SOURCE_SCIM, 5) + field(:ROLE_BINDING_SOURCE_INHERITED_FROM_ORG_ROLE, 6) + field(:ROLE_BINDING_SOURCE_SAML_JIT, 7) +end + +defmodule InternalApi.RBAC.ListUserPermissionsRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:user_id, 1, type: :string, json_name: "userId") + field(:org_id, 2, type: :string, json_name: "orgId") + field(:project_id, 3, type: :string, json_name: "projectId") +end + +defmodule InternalApi.RBAC.ListUserPermissionsResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:user_id, 1, type: :string, json_name: "userId") + field(:org_id, 2, type: :string, json_name: "orgId") + field(:project_id, 3, type: :string, json_name: "projectId") + field(:permissions, 4, repeated: true, type: :string) +end + +defmodule InternalApi.RBAC.ListExistingPermissionsRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:scope, 1, type: InternalApi.RBAC.Scope, enum: true) +end + +defmodule InternalApi.RBAC.ListExistingPermissionsResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:permissions, 1, repeated: true, type: InternalApi.RBAC.Permission) +end + +defmodule InternalApi.RBAC.AssignRoleRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role_assignment, 1, type: InternalApi.RBAC.RoleAssignment, json_name: "roleAssignment") + field(:requester_id, 2, type: :string, json_name: "requesterId") +end + +defmodule InternalApi.RBAC.AssignRoleResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" +end + +defmodule InternalApi.RBAC.RetractRoleRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role_assignment, 1, type: InternalApi.RBAC.RoleAssignment, json_name: "roleAssignment") + field(:requester_id, 2, type: :string, json_name: "requesterId") +end + +defmodule InternalApi.RBAC.RetractRoleResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" +end + +defmodule InternalApi.RBAC.SubjectsHaveRolesRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role_assignments, 1, + repeated: true, + type: InternalApi.RBAC.RoleAssignment, + json_name: "roleAssignments" + ) +end + +defmodule InternalApi.RBAC.SubjectsHaveRolesResponse.HasRole do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role_assignment, 1, type: InternalApi.RBAC.RoleAssignment, json_name: "roleAssignment") + field(:has_role, 2, type: :bool, json_name: "hasRole") +end + +defmodule InternalApi.RBAC.SubjectsHaveRolesResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:has_roles, 1, + repeated: true, + type: InternalApi.RBAC.SubjectsHaveRolesResponse.HasRole, + json_name: "hasRoles" + ) +end + +defmodule InternalApi.RBAC.ListRolesRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") + field(:scope, 2, type: InternalApi.RBAC.Scope, enum: true) +end + +defmodule InternalApi.RBAC.ListRolesResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:roles, 1, repeated: true, type: InternalApi.RBAC.Role) +end + +defmodule InternalApi.RBAC.DescribeRoleRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") + field(:role_id, 2, type: :string, json_name: "roleId") +end + +defmodule InternalApi.RBAC.DescribeRoleResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role, 1, type: InternalApi.RBAC.Role) +end + +defmodule InternalApi.RBAC.ModifyRoleRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role, 1, type: InternalApi.RBAC.Role) + field(:requester_id, 2, type: :string, json_name: "requesterId") +end + +defmodule InternalApi.RBAC.ModifyRoleResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role, 1, type: InternalApi.RBAC.Role) +end + +defmodule InternalApi.RBAC.DestroyRoleRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") + field(:role_id, 2, type: :string, json_name: "roleId") + field(:requester_id, 3, type: :string, json_name: "requesterId") +end + +defmodule InternalApi.RBAC.DestroyRoleResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role_id, 1, type: :string, json_name: "roleId") +end + +defmodule InternalApi.RBAC.ListMembersRequest.Page do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:page_no, 1, type: :int32, json_name: "pageNo") + field(:page_size, 2, type: :int32, json_name: "pageSize") +end + +defmodule InternalApi.RBAC.ListMembersRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") + field(:project_id, 2, type: :string, json_name: "projectId") + field(:member_name_contains, 3, type: :string, json_name: "memberNameContains") + field(:page, 4, type: InternalApi.RBAC.ListMembersRequest.Page) + field(:member_has_role, 5, type: :string, json_name: "memberHasRole") + field(:member_type, 6, type: InternalApi.RBAC.SubjectType, json_name: "memberType", enum: true) +end + +defmodule InternalApi.RBAC.ListMembersResponse.Member do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:subject, 1, type: InternalApi.RBAC.Subject) + + field(:subject_role_bindings, 3, + repeated: true, + type: InternalApi.RBAC.SubjectRoleBinding, + json_name: "subjectRoleBindings" + ) +end + +defmodule InternalApi.RBAC.ListMembersResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:members, 1, repeated: true, type: InternalApi.RBAC.ListMembersResponse.Member) + field(:total_pages, 2, type: :int32, json_name: "totalPages") +end + +defmodule InternalApi.RBAC.CountMembersRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") +end + +defmodule InternalApi.RBAC.CountMembersResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:members, 1, type: :int32) +end + +defmodule InternalApi.RBAC.SubjectRoleBinding do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role, 1, type: InternalApi.RBAC.Role) + field(:source, 2, type: InternalApi.RBAC.RoleBindingSource, enum: true) + field(:role_assigned_at, 3, type: Google.Protobuf.Timestamp, json_name: "roleAssignedAt") +end + +defmodule InternalApi.RBAC.ListAccessibleOrgsRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:user_id, 1, type: :string, json_name: "userId") +end + +defmodule InternalApi.RBAC.ListAccessibleOrgsResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_ids, 1, repeated: true, type: :string, json_name: "orgIds") +end + +defmodule InternalApi.RBAC.ListAccessibleProjectsRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:user_id, 1, type: :string, json_name: "userId") + field(:org_id, 2, type: :string, json_name: "orgId") +end + +defmodule InternalApi.RBAC.ListAccessibleProjectsResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:project_ids, 1, repeated: true, type: :string, json_name: "projectIds") +end + +defmodule InternalApi.RBAC.RoleAssignment do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:role_id, 1, type: :string, json_name: "roleId") + field(:subject, 2, type: InternalApi.RBAC.Subject) + field(:org_id, 3, type: :string, json_name: "orgId") + field(:project_id, 4, type: :string, json_name: "projectId") +end + +defmodule InternalApi.RBAC.Subject do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:subject_type, 1, + type: InternalApi.RBAC.SubjectType, + json_name: "subjectType", + enum: true + ) + + field(:subject_id, 2, type: :string, json_name: "subjectId") + field(:display_name, 3, type: :string, json_name: "displayName") +end + +defmodule InternalApi.RBAC.RefreshCollaboratorsRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") +end + +defmodule InternalApi.RBAC.RefreshCollaboratorsResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" +end + +defmodule InternalApi.RBAC.Role do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:id, 1, type: :string) + field(:name, 2, type: :string) + field(:org_id, 3, type: :string, json_name: "orgId") + field(:scope, 4, type: InternalApi.RBAC.Scope, enum: true) + field(:description, 5, type: :string) + field(:permissions, 6, repeated: true, type: :string) + + field(:rbac_permissions, 7, + repeated: true, + type: InternalApi.RBAC.Permission, + json_name: "rbacPermissions" + ) + + field(:inherited_role, 8, type: InternalApi.RBAC.Role, json_name: "inheritedRole") + field(:maps_to, 9, type: InternalApi.RBAC.Role, json_name: "mapsTo") + field(:readonly, 10, type: :bool) +end + +defmodule InternalApi.RBAC.Permission do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:id, 1, type: :string) + field(:name, 2, type: :string) + field(:description, 3, type: :string) + field(:scope, 4, type: InternalApi.RBAC.Scope, enum: true) +end + +defmodule InternalApi.RBAC.ListSubjectsRequest do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:org_id, 1, type: :string, json_name: "orgId") + field(:subject_ids, 2, repeated: true, type: :string, json_name: "subjectIds") +end + +defmodule InternalApi.RBAC.ListSubjectsResponse do + @moduledoc false + + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" + + field(:subjects, 1, repeated: true, type: InternalApi.RBAC.Subject) +end + +defmodule InternalApi.RBAC.RBAC.Service do + @moduledoc false + + use GRPC.Service, name: "InternalApi.RBAC.RBAC", protoc_gen_elixir_version: "0.13.0" + + rpc( + :ListUserPermissions, + InternalApi.RBAC.ListUserPermissionsRequest, + InternalApi.RBAC.ListUserPermissionsResponse + ) + + rpc( + :ListExistingPermissions, + InternalApi.RBAC.ListExistingPermissionsRequest, + InternalApi.RBAC.ListExistingPermissionsResponse + ) + + rpc(:AssignRole, InternalApi.RBAC.AssignRoleRequest, InternalApi.RBAC.AssignRoleResponse) + + rpc(:RetractRole, InternalApi.RBAC.RetractRoleRequest, InternalApi.RBAC.RetractRoleResponse) + + rpc( + :SubjectsHaveRoles, + InternalApi.RBAC.SubjectsHaveRolesRequest, + InternalApi.RBAC.SubjectsHaveRolesResponse + ) + + rpc(:ListRoles, InternalApi.RBAC.ListRolesRequest, InternalApi.RBAC.ListRolesResponse) + + rpc(:DescribeRole, InternalApi.RBAC.DescribeRoleRequest, InternalApi.RBAC.DescribeRoleResponse) + + rpc(:ModifyRole, InternalApi.RBAC.ModifyRoleRequest, InternalApi.RBAC.ModifyRoleResponse) + + rpc(:DestroyRole, InternalApi.RBAC.DestroyRoleRequest, InternalApi.RBAC.DestroyRoleResponse) + + rpc(:ListMembers, InternalApi.RBAC.ListMembersRequest, InternalApi.RBAC.ListMembersResponse) + + rpc(:CountMembers, InternalApi.RBAC.CountMembersRequest, InternalApi.RBAC.CountMembersResponse) + + rpc( + :ListAccessibleOrgs, + InternalApi.RBAC.ListAccessibleOrgsRequest, + InternalApi.RBAC.ListAccessibleOrgsResponse + ) + + rpc( + :ListAccessibleProjects, + InternalApi.RBAC.ListAccessibleProjectsRequest, + InternalApi.RBAC.ListAccessibleProjectsResponse + ) + + rpc( + :RefreshCollaborators, + InternalApi.RBAC.RefreshCollaboratorsRequest, + InternalApi.RBAC.RefreshCollaboratorsResponse + ) + + rpc(:ListSubjects, InternalApi.RBAC.ListSubjectsRequest, InternalApi.RBAC.ListSubjectsResponse) +end + +defmodule InternalApi.RBAC.RBAC.Stub do + @moduledoc false + + use GRPC.Stub, service: InternalApi.RBAC.RBAC.Service +end diff --git a/ee/ephemeral_environments/scripts/internal_protos.sh b/ee/ephemeral_environments/scripts/internal_protos.sh index 6f930a097..4115a665f 100755 --- a/ee/ephemeral_environments/scripts/internal_protos.sh +++ b/ee/ephemeral_environments/scripts/internal_protos.sh @@ -1,6 +1,7 @@ list=' include/google/protobuf/timestamp ephemeral_environments +rbac ' for element in $list;do From 08f365ea520070430f6b47e8cbb2b12d9c344694 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 16:12:02 +0200 Subject: [PATCH 10/21] rename last_modified_by to last_updated_by --- .../repo/ephemeral_environment_type.ex | 8 ++++---- .../service/ephemeral_environment_type.ex | 2 +- ...2155122_rename_last_modified_by_to_last_updated_by.exs | 7 +++++++ 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 ee/ephemeral_environments/priv/repo/migrations/20251022155122_rename_last_modified_by_to_last_updated_by.exs diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex index ac594e394..5f5332838 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex @@ -10,7 +10,7 @@ defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do field(:name, :string) field(:description, :string) field(:created_by, :binary_id) - field(:last_modified_by, :binary_id) + field(:last_updated_by, :binary_id) field(:state, Ecto.Enum, values: [:draft, :ready, :cordoned, :deleted]) field(:max_number_of_instances, :integer) @@ -24,14 +24,14 @@ defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do :name, :description, :created_by, - :last_modified_by, + :last_updated_by, :state, :max_number_of_instances ]) - |> validate_required([:org_id, :name, :created_by, :last_modified_by, :state]) + |> validate_required([:org_id, :name, :created_by, :last_updated_by, :state]) |> validate_uuid(:org_id) |> validate_uuid(:created_by) - |> validate_uuid(:last_modified_by) + |> validate_uuid(:last_updated_by) |> validate_length(:name, min: 1, max: 255) |> validate_length(:description, max: 1000) |> validate_number(:max_number_of_instances, greater_than: 0) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 5777714f1..07bb801da 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -19,7 +19,7 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do - {:error, String.t()} on validation failure """ def create(attrs) do - attrs = Map.put(attrs, :last_modified_by, attrs[:created_by]) + attrs = Map.put(attrs, :last_updated_by, attrs[:created_by]) attrs = Map.put_new(attrs, :state, :draft) %Schema{} diff --git a/ee/ephemeral_environments/priv/repo/migrations/20251022155122_rename_last_modified_by_to_last_updated_by.exs b/ee/ephemeral_environments/priv/repo/migrations/20251022155122_rename_last_modified_by_to_last_updated_by.exs new file mode 100644 index 000000000..a170db4f5 --- /dev/null +++ b/ee/ephemeral_environments/priv/repo/migrations/20251022155122_rename_last_modified_by_to_last_updated_by.exs @@ -0,0 +1,7 @@ +defmodule EphemeralEnvironments.Repo.Migrations.RenameLastModifiedByToLastUpdatedBy do + use Ecto.Migration + + def change do + rename table(:ephemeral_environment_types), :last_modified_by, to: :last_updated_by + end +end From df76afe1a69c77334b966361e23a18215eb2685d Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 16:14:47 +0200 Subject: [PATCH 11/21] Add proto helper which will convert from proto to map and vice versa --- .../lib/ephemeral_environments/utils/proto.ex | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex index 4f034955f..b098082da 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex @@ -16,13 +16,14 @@ defmodule EphemeralEnvironments.Utils.Proto do def from_map(nil, _module), do: nil def from_map(map, module) when is_map(map) and is_atom(module) do - field_props = module.__message_props__().field_props + field_props = + module.__message_props__().field_props |> Enum.map(fn {_num, props} -> props end) - # Convert map to struct fields + # Convert map to struct fields, only including fields that exist in the schema fields = map + |> Enum.filter(fn {key, _value} -> key in Enum.map(field_props, & &1.name_atom) end) |> Enum.map(fn {key, value} -> - # Find field info for this key field_info = find_field_info(field_props, key) converted_value = convert_value_from_map(value, field_info) {key, converted_value} @@ -53,14 +54,8 @@ defmodule EphemeralEnvironments.Utils.Proto do end def to_map(value), do: value - - defp convert_value(value, module, field) when is_list(value) do - Enum.map(value, &to_map/1) - end - - defp convert_value(value, module, field) when is_struct(value) do - to_map(value) - end + defp convert_value(value, module, field) when is_list(value), do: Enum.map(value, &to_map/1) + defp convert_value(value, module, field) when is_struct(value), do: to_map(value) defp convert_value(value, module, field) when is_integer(value) do # Check if this field is an enum by looking at the field definition @@ -80,20 +75,14 @@ defmodule EphemeralEnvironments.Utils.Proto do defp convert_value(value, _module, _field), do: value - # If given field is of type enum inside the parend module, the name of the enum module + # If given field is of type enum inside the parent module, the name of the enum module # will be returned. Otherwise it will return nil. defp get_enum_module(module, field) do try do - field_props = module.__message_props__().field_props - - # Find the field by name_atom - field_info = - field_props - |> Enum.find(fn {_num, props} -> props.name_atom == field end) - |> case do - {_num, props} -> props - nil -> nil - end + field_props = + module.__message_props__().field_props |> Enum.map(fn {_num, props} -> props end) + + field_info = find_field_info(field_props, field) if field_info && field_info.enum? do case field_info.type do @@ -148,28 +137,18 @@ defmodule EphemeralEnvironments.Utils.Proto do # Find field info by field name atom defp find_field_info(field_props, field_name) do - field_props - |> Enum.find(fn {_num, props} -> props.name_atom == field_name end) - |> case do - {_num, props} -> props - nil -> nil - end + field_props |> Enum.find(fn props -> props.name_atom == field_name end) end defp convert_value_from_map(nil, _field_info), do: nil defp convert_value_from_map(%DateTime{} = dt, _field_info) do - %Google.Protobuf.Timestamp{ - seconds: DateTime.to_unix(dt), - nanos: 0 - } + %Google.Protobuf.Timestamp{seconds: DateTime.to_unix(dt)} end @unix_epoch ~N[1970-01-01 00:00:00] defp convert_value_from_map(%NaiveDateTime{} = ndt, _field_info) do - %Google.Protobuf.Timestamp{ - seconds: NaiveDateTime.diff(ndt, @unix_epoch) - } + %Google.Protobuf.Timestamp{seconds: NaiveDateTime.diff(ndt, @unix_epoch)} end defp convert_value_from_map(value, nil), do: value @@ -188,24 +167,15 @@ defmodule EphemeralEnvironments.Utils.Proto do end end - # Handle nested maps (embedded messages) - defp convert_value_from_map(value, field_info) when is_map(value) and not is_struct(value) do - if field_info.embedded? do - from_map(value, field_info.type) - else - value - end + defp convert_value_from_map(value, field_info) when is_map(value) do + from_map(value, field_info.type) end # Handle enum atoms - convert normalized atom back to proto enum defp convert_value_from_map(value, field_info) when is_atom(value) do - if field_info.enum? do - case field_info.type do - {:enum, enum_module} -> denormalize_enum_name(value, enum_module) - _ -> value - end - else - value + case field_info.type do + {:enum, enum_module} -> denormalize_enum_name(value, enum_module) + _ -> value end end From a8b8fb9334c045b32a6e81179d99db2d6ea4d58e Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 17:20:13 +0200 Subject: [PATCH 12/21] Make sure name us unique per org --- .../repo/ephemeral_environment_type.ex | 4 ++++ .../service/ephemeral_environment_type.ex | 2 +- ...40_add_unique_constraint_to_environment_type_name.exs | 9 +++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 ee/ephemeral_environments/priv/repo/migrations/20251022163740_add_unique_constraint_to_environment_type_name.exs diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex index 5f5332838..719e8ab96 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex @@ -35,6 +35,10 @@ defmodule EphemeralEnvironments.Repo.EphemeralEnvironmentType do |> validate_length(:name, min: 1, max: 255) |> validate_length(:description, max: 1000) |> validate_number(:max_number_of_instances, greater_than: 0) + |> unique_constraint(:duplicate_name, + name: :ephemeral_environment_types_org_id_name_index, + message: "ephemeral environment name has already been taken" + ) end defp validate_uuid(changeset, field) do diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 07bb801da..7d77c6611 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -20,7 +20,7 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do """ def create(attrs) do attrs = Map.put(attrs, :last_updated_by, attrs[:created_by]) - attrs = Map.put_new(attrs, :state, :draft) + attrs = Map.put(attrs, :state, :draft) %Schema{} |> Schema.changeset(attrs) diff --git a/ee/ephemeral_environments/priv/repo/migrations/20251022163740_add_unique_constraint_to_environment_type_name.exs b/ee/ephemeral_environments/priv/repo/migrations/20251022163740_add_unique_constraint_to_environment_type_name.exs new file mode 100644 index 000000000..59156bbb1 --- /dev/null +++ b/ee/ephemeral_environments/priv/repo/migrations/20251022163740_add_unique_constraint_to_environment_type_name.exs @@ -0,0 +1,9 @@ +defmodule EphemeralEnvironments.Repo.Migrations.AddUniqueConstraintToEnvironmentTypeName do + use Ecto.Migration + + def change do + create unique_index(:ephemeral_environment_types, [:org_id, :name], + name: :ephemeral_environment_types_org_id_name_index + ) + end +end From 98e35a338f24167e0121f78dfaf49d1068f0f797 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 17:20:27 +0200 Subject: [PATCH 13/21] Tests for create endpoint --- .../grpc/ephemeral_environments_server.ex | 10 +- .../ephemeral_environments_server_test.exs | 127 ++++++++++++++++++ .../factories/ephemeral_environmets_type.ex | 46 +++++++ 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs create mode 100644 ee/ephemeral_environments/test/support/factories/ephemeral_environmets_type.ex diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index 4ee39a3cc..cb6bb626c 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -25,9 +25,13 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end def create(request, _stream) do - {:ok, ret} = EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) - converted = EphemeralEnvironments.Utils.Proto.from_map(%{environment_type: ret}, CreateResponse) - converted + case EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) do + {:ok, ret} -> + EphemeralEnvironments.Utils.Proto.from_map(%{environment_type: ret}, CreateResponse) + + {:error, error_message} -> + raise GRPC.RPCError, status: :unknown, message: error_message + end end def update(_request, _stream) do diff --git a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs new file mode 100644 index 000000000..3daf52381 --- /dev/null +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -0,0 +1,127 @@ +defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do + use ExUnit.Case, async: false + + alias EphemeralEnvironments.Repo + alias EphemeralEnvironments.Repo.EphemeralEnvironmentType, as: Schema + + alias InternalApi.EphemeralEnvironments.{ + CreateRequest, + EphemeralEnvironmentType, + EphemeralEnvironments + } + + @org_id Ecto.UUID.generate() + @user_id Ecto.UUID.generate() + @grpc_port 50051 + + setup do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo) + + # Allow the gRPC server process to use this test's DB connection + Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()}) + + # Connect to the gRPC server + {:ok, channel} = GRPC.Stub.connect("localhost:#{@grpc_port}") + {:ok, channel: channel} + end + + describe "list/2" do + end + + describe "describe/2" do + end + + describe "create/2" do + test "creates the environment and ignores invalid request attributes", %{channel: channel} do + # Build request with invalid attributes that should be ignored: + # - wrong last_updated_by (should use created_by) + # - wrong state (should default to :draft) + # - old timestamps (should use current DB timestamps) + request = %CreateRequest{ + environment_type: %EphemeralEnvironmentType{ + org_id: @org_id, + name: "Test Environment", + description: "A test environment type", + created_by: @user_id, + last_updated_by: Ecto.UUID.generate(), + state: :TYPE_STATE_CORDONED, + max_number_of_instances: 5, + created_at: build_old_timestamp(), + updated_at: build_old_timestamp() + } + } + + # Make the actual gRPC call through the stub (goes through all interceptors) + {:ok, response} = EphemeralEnvironments.Stub.create(channel, request) + env_type = response.environment_type + + # Validate response - invalid attributes were corrected + assert env_type.org_id == @org_id + assert env_type.name == "Test Environment" + assert env_type.description == "A test environment type" + assert env_type.created_by == @user_id + assert env_type.last_updated_by == @user_id + assert env_type.state == :TYPE_STATE_DRAFT + assert env_type.max_number_of_instances == 5 + assert_recent_timestamp(DateTime.from_unix!(env_type.updated_at.seconds)) + + # Validate database record exists + assert Repo.get(Schema, env_type.id) + end + + test "fails to create environment type with duplicate name in same org", %{channel: channel} do + {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Test") + + duplicate_request = %CreateRequest{ + environment_type: %EphemeralEnvironmentType{ + org_id: @org_id, + name: "Test", + max_number_of_instances: 1, + created_by: @user_id + } + } + + {:error, error} = EphemeralEnvironments.Stub.create(channel, duplicate_request) + assert %GRPC.RPCError{} = error + assert error.status == 2 + assert error.message == "duplicate_name: ephemeral environment name has already been taken" + end + + test "allows same name in different orgs", %{channel: channel} do + {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Test") + + request = %CreateRequest{ + environment_type: %EphemeralEnvironmentType{ + org_id: Ecto.UUID.generate(), + name: "Test", + max_number_of_instances: 1, + created_by: @user_id + } + } + + assert {:ok, _} = EphemeralEnvironments.Stub.create(channel, request) + end + end + + describe "update/2" do + end + + describe "delete/2" do + end + + describe "cordon/2" do + end + + ### + ### Helper functions + ### + + defp build_old_timestamp do + one_hour_ago = DateTime.utc_now() |> DateTime.add(-3600, :second) + %Google.Protobuf.Timestamp{seconds: DateTime.to_unix(one_hour_ago), nanos: 0} + end + + defp assert_recent_timestamp(datetime) do + assert DateTime.diff(DateTime.utc_now(), datetime, :second) < 5 + end +end diff --git a/ee/ephemeral_environments/test/support/factories/ephemeral_environmets_type.ex b/ee/ephemeral_environments/test/support/factories/ephemeral_environmets_type.ex new file mode 100644 index 000000000..eb9c4ec04 --- /dev/null +++ b/ee/ephemeral_environments/test/support/factories/ephemeral_environmets_type.ex @@ -0,0 +1,46 @@ +defmodule Support.Factories.EphemeralEnvironmentsType do + @moduledoc """ + Factory for creating EphemeralEnvironmentType records in tests. + + ## Usage + + # Create with defaults + {:ok, env_type} = Support.Factories.EphemeralEnvironmentsType.insert() + + # Create with custom attributes + {:ok, env_type} = Support.Factories.EphemeralEnvironmentsType.insert( + org_id: "some-org-id", + name: "My Environment", + description: "Custom description", + created_by: "user-id", + state: :ready, + max_number_of_instances: 10 + ) + """ + + def insert(options \\ []) do + attrs = %{ + org_id: get_id(options[:org_id]), + name: get_name(options[:name]), + description: options[:description], + created_by: get_id(options[:created_by]), + last_updated_by: get_id(options[:created_by]), + state: options[:state] || :draft, + max_number_of_instances: options[:max_number_of_instances] || 1 + } + + %EphemeralEnvironments.Repo.EphemeralEnvironmentType{} + |> EphemeralEnvironments.Repo.EphemeralEnvironmentType.changeset(attrs) + |> EphemeralEnvironments.Repo.insert() + end + + defp get_id(nil), do: Ecto.UUID.generate() + defp get_id(id), do: id + + defp get_name(nil), do: "env-" <> random_string(10) + defp get_name(name), do: name + + defp random_string(length) do + for(_ <- 1..length, into: "", do: <>) + end +end From 1bafa4cbfaf2c28123038d6c57f4c10f8c79671d Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 17:36:33 +0200 Subject: [PATCH 14/21] Change proto interceptor to automatically convert to the Reponse struct --- .../grpc/ephemeral_environments_server.ex | 2 +- .../grpc/interceptors/proto_converte.ex | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index cb6bb626c..b2d0dfddf 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -27,7 +27,7 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do def create(request, _stream) do case EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) do {:ok, ret} -> - EphemeralEnvironments.Utils.Proto.from_map(%{environment_type: ret}, CreateResponse) + %{environment_type: ret} {:error, error_message} -> raise GRPC.RPCError, status: :unknown, message: error_message diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex index 4793c7050..83a9e3f61 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex @@ -7,8 +7,12 @@ defmodule EphemeralEnvironments.Grpc.Interceptor.ProtoConverter do end def call(request, stream, next, _) do - Logger.debug("Proto intercepter #{inspect(request)}") - converted = EphemeralEnvironments.Utils.Proto.to_map(request) - next.(converted, stream) + Logger.debug("Proto intercepter - Request: #{inspect(request)}") + converted_request = EphemeralEnvironments.Utils.Proto.to_map(request) + {:ok, stream, response} = next.(converted_request, stream) + + converted_response = EphemeralEnvironments.Utils.Proto.from_map(response, stream.response_mod) + Logger.debug("Proto intercepter - Response: #{inspect(converted_response)}") + {:ok, stream, converted_response} end end From 983766187610ca3025aff7308773836f96674b67 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 17:52:37 +0200 Subject: [PATCH 15/21] Implement and test list endpoint --- .../grpc/ephemeral_environments_server.ex | 10 +++- .../service/ephemeral_environment_type.ex | 23 +++++++- .../ephemeral_environments_server_test.exs | 55 ++++++++++++++++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index b2d0dfddf..e714a193a 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -16,8 +16,14 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do CordonResponse } - def list(_request, _stream) do - %ListResponse{} + def list(request, _stream) do + case EphemeralEnvironments.Service.EphemeralEnvironmentType.list(request.org_id) do + {:ok, environment_types} -> + %{environment_types: environment_types} + + {:error, error_message} -> + raise GRPC.RPCError, status: :unknown, message: error_message + end end def describe(_request, _stream) do diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 7d77c6611..4bc6400c7 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -1,7 +1,27 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do + import Ecto.Query alias EphemeralEnvironments.Repo alias EphemeralEnvironments.Repo.EphemeralEnvironmentType, as: Schema + @doc """ + Lists all ephemeral environment types for a given organization. + + ## Parameters + - org_id: String UUID of the organization + + ## Returns + - {:ok, list of maps} on success + """ + def list(org_id) when is_binary(org_id) do + environment_types = + Schema + |> where([e], e.org_id == ^org_id) + |> Repo.all() + |> Enum.map(&struct_to_map/1) + + {:ok, environment_types} + end + @doc """ Creates a new ephemeral environment type. @@ -9,10 +29,9 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do - attrs: Map with keys: - org_id (required) - name (required) + - max_number_of_instances (required) - created_by (required) - - state (optional, defaults to :draft) - description (optional) - - max_number_of_instances (optional) ## Returns - {:ok, map} on success diff --git a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs index 3daf52381..4b1d7c04b 100644 --- a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -7,7 +7,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do alias InternalApi.EphemeralEnvironments.{ CreateRequest, EphemeralEnvironmentType, - EphemeralEnvironments + EphemeralEnvironments, + ListRequest } @org_id Ecto.UUID.generate() @@ -26,6 +27,58 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do end describe "list/2" do + test "returns empty list when no environment types exist", %{channel: channel} do + request = %ListRequest{org_id: @org_id} + {:ok, response} = EphemeralEnvironments.Stub.list(channel, request) + assert response.environment_types == [] + end + + test "returns all environment types for a specific org", %{channel: channel} do + # Create environment types for the test org + {:ok, env1} = + Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Development") + + {:ok, env1} = + Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Staging") + + # Create environment type for a different org (should not be returned) + {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: Ecto.UUID.generate()) + + request = %ListRequest{org_id: @org_id} + {:ok, response} = EphemeralEnvironments.Stub.list(channel, request) + + assert length(response.environment_types) == 2 + + dev_env = Enum.find(response.environment_types, &(&1.name == "Development")) + assert dev_env.org_id == @org_id + assert dev_env.name == "Development" + + staging_env = Enum.find(response.environment_types, &(&1.name == "Staging")) + assert staging_env.org_id == @org_id + assert staging_env.name == "Staging" + end + + test "handles multiple orgs correctly", %{channel: channel} do + org2_id = Ecto.UUID.generate() + + {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) + {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) + + # Create environment types for org2 + {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: org2_id) + + # Request for org1 + request1 = %ListRequest{org_id: @org_id} + {:ok, response1} = EphemeralEnvironments.Stub.list(channel, request1) + assert length(response1.environment_types) == 2 + assert Enum.all?(response1.environment_types, &(&1.org_id == @org_id)) + + # Request for org2 + request2 = %ListRequest{org_id: org2_id} + {:ok, response2} = EphemeralEnvironments.Stub.list(channel, request2) + assert length(response2.environment_types) == 1 + assert Enum.all?(response2.environment_types, &(&1.org_id == org2_id)) + end end describe "describe/2" do From 13a4d1d434f49ad869eb5c08bf446ffc6f3d7e2e Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 18:07:52 +0200 Subject: [PATCH 16/21] Write and test describe endpoint --- .../grpc/ephemeral_environments_server.ex | 17 ++++- .../service/ephemeral_environment_type.ex | 29 ++++++++ .../ephemeral_environments_server_test.exs | 74 +++++++++++++++---- 3 files changed, 102 insertions(+), 18 deletions(-) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index e714a193a..032f18bd6 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -26,8 +26,21 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end end - def describe(_request, _stream) do - %DescribeResponse{} + def describe(request, _stream) do + case EphemeralEnvironments.Service.EphemeralEnvironmentType.describe( + request.id, + request.org_id + ) do + {:ok, environment_type} -> + # Note: instances field will be added once we implement instance management + %{environment_type: environment_type, instances: []} + + {:error, :not_found} -> + raise GRPC.RPCError, status: :not_found, message: "Environment type not found" + + {:error, error_message} -> + raise GRPC.RPCError, status: :unknown, message: error_message + end end def create(request, _stream) do diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 4bc6400c7..7f93fe643 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -22,6 +22,27 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do {:ok, environment_types} end + @doc """ + Describes a specific ephemeral environment type by ID and org_id. + + ## Parameters + - id: String UUID of the environment type + - org_id: String UUID of the organization + + ## Returns + - {:ok, map} on success + - {:error, :not_found} if the environment type doesn't exist + """ + def describe(id, org_id) when is_binary(id) and is_binary(org_id) do + Schema + |> where([e], e.id == ^id and e.org_id == ^org_id) + |> Repo.one() + |> case do + nil -> {:error, :not_found} + record -> {:ok, struct_to_map(record)} + end + end + @doc """ Creates a new ephemeral environment type. @@ -58,6 +79,14 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do struct |> Map.from_struct() |> Map.drop([:__meta__]) + |> rename_timestamp_fields() + end + + # Rename Ecto's inserted_at to created_at to match proto definition + defp rename_timestamp_fields(map) do + map + |> Map.put(:created_at, map[:inserted_at]) + |> Map.delete(:inserted_at) end defp format_errors(changeset) do diff --git a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs index 4b1d7c04b..e9330b16a 100644 --- a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -3,12 +3,14 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do alias EphemeralEnvironments.Repo alias EphemeralEnvironments.Repo.EphemeralEnvironmentType, as: Schema + alias Support.Factories alias InternalApi.EphemeralEnvironments.{ CreateRequest, EphemeralEnvironmentType, EphemeralEnvironments, - ListRequest + ListRequest, + DescribeRequest } @org_id Ecto.UUID.generate() @@ -35,14 +37,11 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do test "returns all environment types for a specific org", %{channel: channel} do # Create environment types for the test org - {:ok, env1} = - Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Development") - - {:ok, env1} = - Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Staging") + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Development") + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Staging") # Create environment type for a different org (should not be returned) - {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: Ecto.UUID.generate()) + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: Ecto.UUID.generate()) request = %ListRequest{org_id: @org_id} {:ok, response} = EphemeralEnvironments.Stub.list(channel, request) @@ -61,11 +60,11 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do test "handles multiple orgs correctly", %{channel: channel} do org2_id = Ecto.UUID.generate() - {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) - {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) # Create environment types for org2 - {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: org2_id) + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: org2_id) # Request for org1 request1 = %ListRequest{org_id: @org_id} @@ -82,6 +81,50 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do end describe "describe/2" do + test "returns environment type when it exists", %{channel: channel} do + {:ok, env_type} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Production", + description: "Production environment", + created_by: @user_id, + state: :ready, + max_number_of_instances: 20 + ) + + request = %DescribeRequest{id: env_type.id, org_id: @org_id} + + {:ok, response} = EphemeralEnvironments.Stub.describe(channel, request) + + assert response.environment_type.id == env_type.id + assert response.environment_type.org_id == @org_id + assert response.environment_type.name == "Production" + assert response.environment_type.description == "Production environment" + assert response.environment_type.created_by == @user_id + assert response.environment_type.last_updated_by == @user_id + assert response.environment_type.state == :TYPE_STATE_READY + assert response.environment_type.max_number_of_instances == 20 + assert_recent_timestamp(DateTime.from_unix!(response.environment_type.created_at.seconds)) + assert_recent_timestamp(DateTime.from_unix!(response.environment_type.updated_at.seconds)) + assert response.instances == [] + end + + test "returns not_found error when environment type doesn't exist", %{channel: channel} do + request = %DescribeRequest{id: Ecto.UUID.generate(), org_id: @org_id} + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.describe(channel, request) + + assert error.status == 5 + assert error.message == "Environment type not found" + end + + test "returns not_found when querying with wrong org_id", %{channel: channel} do + {:ok, env_type} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) + request = %DescribeRequest{id: env_type.id, org_id: Ecto.UUID.generate()} + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.describe(channel, request) + + assert error.status == 5 + assert error.message == "Environment type not found" + end end describe "create/2" do @@ -117,20 +160,20 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do assert env_type.state == :TYPE_STATE_DRAFT assert env_type.max_number_of_instances == 5 assert_recent_timestamp(DateTime.from_unix!(env_type.updated_at.seconds)) + assert_recent_timestamp(DateTime.from_unix!(env_type.created_at.seconds)) # Validate database record exists assert Repo.get(Schema, env_type.id) end test "fails to create environment type with duplicate name in same org", %{channel: channel} do - {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Test") + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Test") duplicate_request = %CreateRequest{ environment_type: %EphemeralEnvironmentType{ org_id: @org_id, name: "Test", - max_number_of_instances: 1, - created_by: @user_id + max_number_of_instances: 1 } } @@ -141,14 +184,13 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do end test "allows same name in different orgs", %{channel: channel} do - {:ok, _} = Support.Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Test") + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Test") request = %CreateRequest{ environment_type: %EphemeralEnvironmentType{ org_id: Ecto.UUID.generate(), name: "Test", - max_number_of_instances: 1, - created_by: @user_id + max_number_of_instances: 1 } } From bdf64c1f37b30b9be87c8c298209d2d4ccc638a1 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Wed, 22 Oct 2025 18:16:19 +0200 Subject: [PATCH 17/21] Implement and test update endpoint --- .../grpc/ephemeral_environments_server.ex | 13 +- .../service/ephemeral_environment_type.ex | 73 ++++++- .../ephemeral_environments_server_test.exs | 203 +++++++++++++++++- 3 files changed, 285 insertions(+), 4 deletions(-) diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index 032f18bd6..ab5f8afa2 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -53,8 +53,17 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end end - def update(_request, _stream) do - %UpdateResponse{} + def update(request, _stream) do + case EphemeralEnvironments.Service.EphemeralEnvironmentType.update(request.environment_type) do + {:ok, environment_type} -> + %{environment_type: environment_type} + + {:error, :not_found} -> + raise GRPC.RPCError, status: :not_found, message: "Environment type not found" + + {:error, error_message} -> + raise GRPC.RPCError, status: :unknown, message: error_message + end end def delete(_request, _stream) do diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 7f93fe643..018addc8a 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -43,6 +43,70 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do end end + @doc """ + Updates an existing ephemeral environment type. + + ## Parameters + - attrs: Map with keys: + - id (required) + - org_id (required) + - last_updated_by (required) + - name (optional) + - description (optional) + - max_number_of_instances (optional) + - state (optional) + + ## Returns + - {:ok, map} on success + - {:error, :not_found} if the environment type doesn't exist + - {:error, String.t()} on validation failure + """ + def update(attrs) do + # Filter out proto default values that shouldn't be updated + attrs = filter_proto_defaults(attrs) + + with {:ok, record} <- get_record(attrs[:id], attrs[:org_id]), + {:ok, updated_record} <- update_record(record, attrs) do + {:ok, struct_to_map(updated_record)} + end + end + + # Remove proto default values that indicate "not set" rather than explicit values + defp filter_proto_defaults(attrs) do + attrs + |> Enum.reject(fn + # Empty strings from proto mean "not set" + {_key, ""} -> true + # :unspecified enum means "not set" + {:state, :unspecified} -> true + # 0 for max_number_of_instances means "not set" (since validation requires > 0) + {:max_number_of_instances, 0} -> true + # Keep everything else + _ -> false + end) + |> Map.new() + end + + defp get_record(id, org_id) when is_binary(id) and is_binary(org_id) do + Schema + |> where([e], e.id == ^id and e.org_id == ^org_id) + |> Repo.one() + |> case do + nil -> {:error, :not_found} + record -> {:ok, record} + end + end + + defp update_record(record, attrs) do + record + |> Schema.changeset(attrs) + |> Repo.update() + |> case do + {:ok, updated_record} -> {:ok, updated_record} + {:error, changeset} -> {:error, format_errors(changeset)} + end + end + @doc """ Creates a new ephemeral environment type. @@ -92,7 +156,7 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do defp format_errors(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> Enum.reduce(opts, msg, fn {key, value}, acc -> - String.replace(acc, "%{#{key}}", to_string(value)) + String.replace(acc, "%{#{key}}", safe_to_string(value)) end) end) |> Enum.map(fn {field, errors} -> @@ -100,4 +164,11 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do end) |> Enum.join("; ") end + + # Safely convert values to strings, handling complex types + defp safe_to_string(value) when is_binary(value), do: value + defp safe_to_string(value) when is_atom(value), do: to_string(value) + defp safe_to_string(value) when is_number(value), do: to_string(value) + defp safe_to_string(value) when is_list(value), do: inspect(value) + defp safe_to_string(value), do: inspect(value) end diff --git a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs index e9330b16a..9d46ed1f5 100644 --- a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -10,7 +10,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do EphemeralEnvironmentType, EphemeralEnvironments, ListRequest, - DescribeRequest + DescribeRequest, + UpdateRequest } @org_id Ecto.UUID.generate() @@ -199,6 +200,206 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do end describe "update/2" do + test "updates environment type successfully", %{channel: channel} do + {:ok, env_type} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Original Name", + description: "Original description", + created_by: @user_id, + state: :draft, + max_number_of_instances: 5 + ) + + updater_id = Ecto.UUID.generate() + + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: env_type.id, + org_id: @org_id, + name: "Updated Name", + description: "Updated description", + last_updated_by: updater_id, + state: :TYPE_STATE_READY, + max_number_of_instances: 10 + } + } + + {:ok, response} = EphemeralEnvironments.Stub.update(channel, request) + + assert response.environment_type.id == env_type.id + assert response.environment_type.org_id == @org_id + assert response.environment_type.name == "Updated Name" + assert response.environment_type.description == "Updated description" + assert response.environment_type.last_updated_by == updater_id + assert response.environment_type.state == :TYPE_STATE_READY + assert response.environment_type.max_number_of_instances == 10 + # created_by should remain unchanged + assert response.environment_type.created_by == @user_id + + # Verify database record was updated + db_record = Repo.get(Schema, env_type.id) + assert db_record.name == "Updated Name" + assert db_record.description == "Updated description" + assert db_record.last_updated_by == updater_id + assert db_record.state == :ready + end + + test "updates only provided fields", %{channel: channel} do + {:ok, env_type} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Original Name", + description: "Original description", + created_by: @user_id, + state: :draft, + max_number_of_instances: 5 + ) + + updater_id = Ecto.UUID.generate() + + # Only update name and last_updated_by + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: env_type.id, + org_id: @org_id, + name: "New Name", + last_updated_by: updater_id + } + } + + {:ok, response} = EphemeralEnvironments.Stub.update(channel, request) + + assert response.environment_type.name == "New Name" + assert response.environment_type.last_updated_by == updater_id + # Other fields should remain unchanged + assert response.environment_type.description == "Original description" + assert response.environment_type.state == :TYPE_STATE_DRAFT + assert response.environment_type.max_number_of_instances == 5 + end + + test "returns not_found when environment type doesn't exist", %{channel: channel} do + non_existent_id = Ecto.UUID.generate() + + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: non_existent_id, + org_id: @org_id, + name: "Updated Name", + last_updated_by: @user_id + } + } + + assert {:error, %GRPC.RPCError{status: 5, message: "Environment type not found"}} = + EphemeralEnvironments.Stub.update(channel, request) + end + + test "returns not_found when updating with wrong org_id", %{channel: channel} do + {:ok, env_type} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Test Environment" + ) + + different_org_id = Ecto.UUID.generate() + + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: env_type.id, + org_id: different_org_id, + name: "Updated Name", + last_updated_by: @user_id + } + } + + assert {:error, %GRPC.RPCError{status: 5, message: "Environment type not found"}} = + EphemeralEnvironments.Stub.update(channel, request) + end + + test "fails validation when updating with duplicate name in same org", %{channel: channel} do + {:ok, env1} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Environment 1" + ) + + {:ok, env2} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Environment 2" + ) + + # Try to rename env2 to env1's name + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: env2.id, + org_id: @org_id, + name: "Environment 1", + last_updated_by: @user_id + } + } + + {:error, error} = EphemeralEnvironments.Stub.update(channel, request) + assert %GRPC.RPCError{} = error + # UNKNOWN + assert error.status == 2 + assert error.message == "duplicate_name: ephemeral environment name has already been taken" + end + + test "allows updating to same name in different org", %{channel: channel} do + {:ok, env1} = + Factories.EphemeralEnvironmentsType.insert( + org_id: @org_id, + name: "Shared Name" + ) + + other_org_id = Ecto.UUID.generate() + + {:ok, env2} = + Factories.EphemeralEnvironmentsType.insert( + org_id: other_org_id, + name: "Original Name" + ) + + # Update env2 to use the same name as env1 (but different org) + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: env2.id, + org_id: other_org_id, + name: "Shared Name", + last_updated_by: @user_id + } + } + + assert {:ok, response} = EphemeralEnvironments.Stub.update(channel, request) + assert response.environment_type.name == "Shared Name" + end + + test "updates timestamp when updating", %{channel: channel} do + {:ok, env_type} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) + + # Wait a bit to ensure timestamp changes + :timer.sleep(100) + + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: env_type.id, + org_id: @org_id, + name: "Updated Name", + last_updated_by: @user_id + } + } + + {:ok, response} = EphemeralEnvironments.Stub.update(channel, request) + + # created_at should be the original timestamp + original_created_at = DateTime.from_naive!(env_type.inserted_at, "Etc/UTC") + response_created_at = DateTime.from_unix!(response.environment_type.created_at.seconds) + assert DateTime.diff(response_created_at, original_created_at, :second) == 0 + + # updated_at should be recent + assert_recent_timestamp(DateTime.from_unix!(response.environment_type.updated_at.seconds)) + end end describe "delete/2" do From ffb6efd85b9321b24031005ed0a60e7ac11ca847 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 23 Oct 2025 10:05:11 +0200 Subject: [PATCH 18/21] Fix linting warnings --- .../grpc/ephemeral_environments_server.ex | 35 +++----------- .../service/ephemeral_environment_type.ex | 3 +- .../lib/ephemeral_environments/utils/proto.ex | 46 +++++++++---------- .../include/google/protobuf/timestamp.pb.ex | 8 ---- .../scripts/internal_protos.sh | 1 - .../ephemeral_environments_server_test.exs | 2 +- 6 files changed, 29 insertions(+), 66 deletions(-) delete mode 100644 ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex index ab5f8afa2..60e7d98c3 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -1,36 +1,15 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do use GRPC.Server, service: InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service - alias InternalApi.EphemeralEnvironments.{ - ListRequest, - ListResponse, - DescribeRequest, - DescribeResponse, - CreateRequest, - CreateResponse, - UpdateRequest, - UpdateResponse, - DeleteRequest, - DeleteResponse, - CordonRequest, - CordonResponse - } + alias EphemeralEnvironments.Service.EphemeralEnvironmentType def list(request, _stream) do - case EphemeralEnvironments.Service.EphemeralEnvironmentType.list(request.org_id) do - {:ok, environment_types} -> - %{environment_types: environment_types} - - {:error, error_message} -> - raise GRPC.RPCError, status: :unknown, message: error_message - end + {:ok, environment_types} = EphemeralEnvironmentType.list(request.org_id) + %{environment_types: environment_types} end def describe(request, _stream) do - case EphemeralEnvironments.Service.EphemeralEnvironmentType.describe( - request.id, - request.org_id - ) do + case EphemeralEnvironmentType.describe(request.id, request.org_id) do {:ok, environment_type} -> # Note: instances field will be added once we implement instance management %{environment_type: environment_type, instances: []} @@ -44,7 +23,7 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end def create(request, _stream) do - case EphemeralEnvironments.Service.EphemeralEnvironmentType.create(request.environment_type) do + case EphemeralEnvironmentType.create(request.environment_type) do {:ok, ret} -> %{environment_type: ret} @@ -54,7 +33,7 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end def update(request, _stream) do - case EphemeralEnvironments.Service.EphemeralEnvironmentType.update(request.environment_type) do + case EphemeralEnvironmentType.update(request.environment_type) do {:ok, environment_type} -> %{environment_type: environment_type} @@ -67,10 +46,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do end def delete(_request, _stream) do - %DeleteResponse{} end def cordon(_request, _stream) do - %CordonResponse{} end end diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex index 018addc8a..ddef08bdb 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -159,10 +159,9 @@ defmodule EphemeralEnvironments.Service.EphemeralEnvironmentType do String.replace(acc, "%{#{key}}", safe_to_string(value)) end) end) - |> Enum.map(fn {field, errors} -> + |> Enum.map_join("; ", fn {field, errors} -> "#{field}: #{Enum.join(errors, ", ")}" end) - |> Enum.join("; ") end # Safely convert values to strings, handling complex types diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex index b098082da..f9cd7ca48 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex @@ -54,8 +54,8 @@ defmodule EphemeralEnvironments.Utils.Proto do end def to_map(value), do: value - defp convert_value(value, module, field) when is_list(value), do: Enum.map(value, &to_map/1) - defp convert_value(value, module, field) when is_struct(value), do: to_map(value) + defp convert_value(value, _module, _field) when is_list(value), do: Enum.map(value, &to_map/1) + defp convert_value(value, _module, _field) when is_struct(value), do: to_map(value) defp convert_value(value, module, field) when is_integer(value) do # Check if this field is an enum by looking at the field definition @@ -78,37 +78,33 @@ defmodule EphemeralEnvironments.Utils.Proto do # If given field is of type enum inside the parent module, the name of the enum module # will be returned. Otherwise it will return nil. defp get_enum_module(module, field) do - try do - field_props = - module.__message_props__().field_props |> Enum.map(fn {_num, props} -> props end) + field_props = + module.__message_props__().field_props |> Enum.map(fn {_num, props} -> props end) - field_info = find_field_info(field_props, field) + field_info = find_field_info(field_props, field) - if field_info && field_info.enum? do - case field_info.type do - {:enum, enum_module} -> enum_module - _ -> nil - end - else - nil + if field_info && field_info.enum? do + case field_info.type do + {:enum, enum_module} -> enum_module + _ -> nil end - rescue - _ -> nil + else + nil end + rescue + _ -> nil end defp integer_to_atom(enum_module, value) do - try do - enum_module.__message_props__() - |> Map.get(:field_props, %{}) - |> Enum.find(fn {_name, props} -> props[:enum_value] == value end) - |> case do - {name, _} -> normalize_enum_name(name, enum_module) - nil -> value - end - rescue - _ -> value + enum_module.__message_props__() + |> Map.get(:field_props, %{}) + |> Enum.find(fn {_name, props} -> props[:enum_value] == value end) + |> case do + {name, _} -> normalize_enum_name(name, enum_module) + nil -> value end + rescue + _ -> value end # Normalize enum names by removing prefix and lowercasing diff --git a/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex b/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex deleted file mode 100644 index 410019fb7..000000000 --- a/ee/ephemeral_environments/lib/internal_api/include/google/protobuf/timestamp.pb.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Google.Protobuf.Timestamp do - @moduledoc false - - use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.13.0" - - field(:seconds, 1, type: :int64) - field(:nanos, 2, type: :int32) -end diff --git a/ee/ephemeral_environments/scripts/internal_protos.sh b/ee/ephemeral_environments/scripts/internal_protos.sh index 4115a665f..8304b14f9 100755 --- a/ee/ephemeral_environments/scripts/internal_protos.sh +++ b/ee/ephemeral_environments/scripts/internal_protos.sh @@ -1,5 +1,4 @@ list=' -include/google/protobuf/timestamp ephemeral_environments rbac ' diff --git a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs index 9d46ed1f5..b8902d8ba 100644 --- a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -16,7 +16,7 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do @org_id Ecto.UUID.generate() @user_id Ecto.UUID.generate() - @grpc_port 50051 + @grpc_port 50_051 setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo) From 62511a83694c7e3adbad970babd8247cd9d080a0 Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 23 Oct 2025 11:36:46 +0200 Subject: [PATCH 19/21] Fix failing tests --- .../ephemeral_environments_server_test.exs | 84 ++++++------------- 1 file changed, 27 insertions(+), 57 deletions(-) diff --git a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs index b8902d8ba..4ff6e63e1 100644 --- a/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -24,7 +24,6 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do # Allow the gRPC server process to use this test's DB connection Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()}) - # Connect to the gRPC server {:ok, channel} = GRPC.Stub.connect("localhost:#{@grpc_port}") {:ok, channel: channel} end @@ -40,7 +39,6 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do # Create environment types for the test org {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Development") {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "Staging") - # Create environment type for a different org (should not be returned) {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: Ecto.UUID.generate()) @@ -60,10 +58,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do test "handles multiple orgs correctly", %{channel: channel} do org2_id = Ecto.UUID.generate() - {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) - # Create environment types for org2 {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: org2_id) @@ -112,8 +108,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do test "returns not_found error when environment type doesn't exist", %{channel: channel} do request = %DescribeRequest{id: Ecto.UUID.generate(), org_id: @org_id} - {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.describe(channel, request) + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.describe(channel, request) assert error.status == 5 assert error.message == "Environment type not found" end @@ -121,8 +117,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do test "returns not_found when querying with wrong org_id", %{channel: channel} do {:ok, env_type} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) request = %DescribeRequest{id: env_type.id, org_id: Ecto.UUID.generate()} - {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.describe(channel, request) + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.describe(channel, request) assert error.status == 5 assert error.message == "Environment type not found" end @@ -174,12 +170,14 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do environment_type: %EphemeralEnvironmentType{ org_id: @org_id, name: "Test", - max_number_of_instances: 1 + max_number_of_instances: 1, + created_by: @user_id } } - {:error, error} = EphemeralEnvironments.Stub.create(channel, duplicate_request) - assert %GRPC.RPCError{} = error + {:error, %GRPC.RPCError{} = error} = + EphemeralEnvironments.Stub.create(channel, duplicate_request) + assert error.status == 2 assert error.message == "duplicate_name: ephemeral environment name has already been taken" end @@ -191,7 +189,8 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do environment_type: %EphemeralEnvironmentType{ org_id: Ecto.UUID.generate(), name: "Test", - max_number_of_instances: 1 + max_number_of_instances: 1, + created_by: @user_id } } @@ -279,28 +278,22 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do end test "returns not_found when environment type doesn't exist", %{channel: channel} do - non_existent_id = Ecto.UUID.generate() - request = %UpdateRequest{ environment_type: %EphemeralEnvironmentType{ - id: non_existent_id, + id: Ecto.UUID.generate(), org_id: @org_id, name: "Updated Name", last_updated_by: @user_id } } - assert {:error, %GRPC.RPCError{status: 5, message: "Environment type not found"}} = - EphemeralEnvironments.Stub.update(channel, request) + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.update(channel, request) + assert error.status == 5 + assert error.message == "Environment type not found" end test "returns not_found when updating with wrong org_id", %{channel: channel} do - {:ok, env_type} = - Factories.EphemeralEnvironmentsType.insert( - org_id: @org_id, - name: "Test Environment" - ) - + {:ok, env_type} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "1") different_org_id = Ecto.UUID.generate() request = %UpdateRequest{ @@ -312,72 +305,51 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do } } - assert {:error, %GRPC.RPCError{status: 5, message: "Environment type not found"}} = - EphemeralEnvironments.Stub.update(channel, request) + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.update(channel, request) + assert error.status == 5 + assert error.message == "Environment type not found" end test "fails validation when updating with duplicate name in same org", %{channel: channel} do - {:ok, env1} = - Factories.EphemeralEnvironmentsType.insert( - org_id: @org_id, - name: "Environment 1" - ) - - {:ok, env2} = - Factories.EphemeralEnvironmentsType.insert( - org_id: @org_id, - name: "Environment 2" - ) + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "1") + {:ok, env2} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "2") # Try to rename env2 to env1's name request = %UpdateRequest{ environment_type: %EphemeralEnvironmentType{ id: env2.id, org_id: @org_id, - name: "Environment 1", + name: "1", last_updated_by: @user_id } } - {:error, error} = EphemeralEnvironments.Stub.update(channel, request) - assert %GRPC.RPCError{} = error - # UNKNOWN + {:error, %GRPC.RPCError{} = error} = EphemeralEnvironments.Stub.update(channel, request) assert error.status == 2 assert error.message == "duplicate_name: ephemeral environment name has already been taken" end test "allows updating to same name in different org", %{channel: channel} do - {:ok, env1} = - Factories.EphemeralEnvironmentsType.insert( - org_id: @org_id, - name: "Shared Name" - ) - - other_org_id = Ecto.UUID.generate() - - {:ok, env2} = - Factories.EphemeralEnvironmentsType.insert( - org_id: other_org_id, - name: "Original Name" - ) + {:ok, _} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id, name: "1") + org2_id = Ecto.UUID.generate() + {:ok, env2} = Factories.EphemeralEnvironmentsType.insert(org_id: org2_id, name: "2") # Update env2 to use the same name as env1 (but different org) request = %UpdateRequest{ environment_type: %EphemeralEnvironmentType{ id: env2.id, - org_id: other_org_id, - name: "Shared Name", + org_id: org2_id, + name: "1", last_updated_by: @user_id } } assert {:ok, response} = EphemeralEnvironments.Stub.update(channel, request) - assert response.environment_type.name == "Shared Name" + assert response.environment_type.name == "1" end test "updates timestamp when updating", %{channel: channel} do {:ok, env_type} = Factories.EphemeralEnvironmentsType.insert(org_id: @org_id) - # Wait a bit to ensure timestamp changes :timer.sleep(100) @@ -396,8 +368,6 @@ defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do original_created_at = DateTime.from_naive!(env_type.inserted_at, "Etc/UTC") response_created_at = DateTime.from_unix!(response.environment_type.created_at.seconds) assert DateTime.diff(response_created_at, original_created_at, :second) == 0 - - # updated_at should be recent assert_recent_timestamp(DateTime.from_unix!(response.environment_type.updated_at.seconds)) end end From 8cc02d50a0b8166b89a03e9e138944385a58738f Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 23 Oct 2025 11:39:23 +0200 Subject: [PATCH 20/21] Remove empty dummy test --- ee/ephemeral_environments/test/empty_test.exs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 ee/ephemeral_environments/test/empty_test.exs diff --git a/ee/ephemeral_environments/test/empty_test.exs b/ee/ephemeral_environments/test/empty_test.exs deleted file mode 100644 index 78d186359..000000000 --- a/ee/ephemeral_environments/test/empty_test.exs +++ /dev/null @@ -1,12 +0,0 @@ -defmodule EphemeralEnvironmentTypesTest do - use EphemeralEnvironments.RepoCase - - test "list all ephemeral environment types" do - result = Repo.query!("SELECT * FROM ephemeral_environment_types") - - IO.puts("Columns: #{inspect(result.columns)}") - IO.puts("Rows: #{inspect(result.rows)}") - - assert is_list(result.rows) - end -end From c3d44d09ce14ebcc3ec2d232bc1e4ce7942da87d Mon Sep 17 00:00:00 2001 From: VeljkoMaksimovic Date: Thu, 23 Oct 2025 11:46:03 +0200 Subject: [PATCH 21/21] Fix test reports --- ee/ephemeral_environments/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/ee/ephemeral_environments/Makefile b/ee/ephemeral_environments/Makefile index 7e07dfa25..c7ddff45c 100644 --- a/ee/ephemeral_environments/Makefile +++ b/ee/ephemeral_environments/Makefile @@ -10,6 +10,7 @@ TMP_REPO_DIR?=/tmp/internal_api CONTAINER_ENV_VARS = $(shell [ -f .env ] && sed 's/^/-e /' .env | tr '\n' ' ') ifdef CI CONTAINER_ENV_VARS := $(shell echo "$(CONTAINER_ENV_VARS)" | sed 's/-e POSTGRES_DB_HOST=[^ ]*/-e POSTGRES_DB_HOST=0.0.0.0/g') +CONTAINER_ENV_VARS += -e CI=true $(info CI detected - CONTAINER_ENV_VARS: $(CONTAINER_ENV_VARS)) endif