diff --git a/ee/ephemeral_environments/Makefile b/ee/ephemeral_environments/Makefile index b997d1f0b..c7ddff45c 100644 --- a/ee/ephemeral_environments/Makefile +++ b/ee/ephemeral_environments/Makefile @@ -3,13 +3,14 @@ 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 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 @@ -23,6 +24,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/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 diff --git a/ee/ephemeral_environments/lib/ephemeral_environments/application.ex b/ee/ephemeral_environments/lib/ephemeral_environments/application.ex index 5fa2848ae..18f947b2a 100644 --- a/ee/ephemeral_environments/lib/ephemeral_environments/application.ex +++ b/ee/ephemeral_environments/lib/ephemeral_environments/application.ex @@ -9,7 +9,13 @@ defmodule EphemeralEnvironments.Application do "Starting EphemeralEnvironments in '#{Application.get_env(:ephemeral_environments, :env)}' environment" ) - children = [EphemeralEnvironments.Repo] + children = [ + EphemeralEnvironments.Repo, + {GRPC.Server.Supervisor, + endpoint: EphemeralEnvironments.Grpc.Endpoint, + port: Application.fetch_env!(:ephemeral_environments, :grpc_listen_port), + start_server: true} + ] 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/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/ephemeral_environments/grpc/ephemeral_environments_server.ex b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex new file mode 100644 index 000000000..60e7d98c3 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/ephemeral_environments_server.ex @@ -0,0 +1,53 @@ +defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServer do + use GRPC.Server, service: InternalApi.EphemeralEnvironments.EphemeralEnvironments.Service + + alias EphemeralEnvironments.Service.EphemeralEnvironmentType + + def list(request, _stream) do + {:ok, environment_types} = EphemeralEnvironmentType.list(request.org_id) + %{environment_types: environment_types} + end + + def describe(request, _stream) 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: []} + + {: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 + case EphemeralEnvironmentType.create(request.environment_type) do + {:ok, ret} -> + %{environment_type: ret} + + {:error, error_message} -> + raise GRPC.RPCError, status: :unknown, message: error_message + end + end + + def update(request, _stream) do + case 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 + end + + def cordon(_request, _stream) do + end +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..83a9e3f61 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/grpc/interceptors/proto_converte.ex @@ -0,0 +1,18 @@ +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 - 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 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..719e8ab96 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/repo/ephemeral_environment_type.ex @@ -0,0 +1,52 @@ +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_updated_by, :binary_id) + field(:state, Ecto.Enum, values: [:draft, :ready, :cordoned, :deleted]) + field(:max_number_of_instances, :integer) + + timestamps() + end + + def changeset(ephemeral_environment_type, attrs) do + ephemeral_environment_type + |> cast(attrs, [ + :org_id, + :name, + :description, + :created_by, + :last_updated_by, + :state, + :max_number_of_instances + ]) + |> validate_required([:org_id, :name, :created_by, :last_updated_by, :state]) + |> validate_uuid(:org_id) + |> validate_uuid(:created_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) + |> 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 + 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..ddef08bdb --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/service/ephemeral_environment_type.ex @@ -0,0 +1,173 @@ +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 """ + 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 """ + 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. + + ## Parameters + - attrs: Map with keys: + - org_id (required) + - name (required) + - max_number_of_instances (required) + - created_by (required) + - description (optional) + + ## Returns + - {:ok, map} on success + - {:error, String.t()} on validation failure + """ + def create(attrs) do + attrs = Map.put(attrs, :last_updated_by, attrs[:created_by]) + attrs = Map.put(attrs, :state, :draft) + + %Schema{} + |> Schema.changeset(attrs) + |> Repo.insert() + |> case do + {: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__]) + |> 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 + Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", safe_to_string(value)) + end) + end) + |> Enum.map_join("; ", fn {field, errors} -> + "#{field}: #{Enum.join(errors, ", ")}" + end) + 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/lib/ephemeral_environments/utils/proto.ex b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex new file mode 100644 index 000000000..f9cd7ca48 --- /dev/null +++ b/ee/ephemeral_environments/lib/ephemeral_environments/utils/proto.ex @@ -0,0 +1,190 @@ +defmodule EphemeralEnvironments.Utils.Proto do + @moduledoc """ + 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 |> Enum.map(fn {_num, props} -> props end) + + # 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} -> + 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 + - 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) + 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 + 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 parent module, the name of the enum module + # will be returned. Otherwise it will return nil. + defp get_enum_module(module, field) do + 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 + {:enum, enum_module} -> enum_module + _ -> nil + end + else + nil + end + rescue + _ -> nil + end + + defp integer_to_atom(enum_module, value) 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 + + # 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 + + # Find field info by field name atom + defp find_field_info(field_props, field_name) do + 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)} + 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 + + 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 + case field_info.type do + {:enum, enum_module} -> denormalize_enum_name(value, enum_module) + _ -> 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 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..9287abca7 --- /dev/null +++ b/ee/ephemeral_environments/lib/internal_api/ephemeral_environments.pb.ex @@ -0,0 +1,443 @@ +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 + + 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.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.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.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") + 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 + @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 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/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 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 diff --git a/ee/ephemeral_environments/scripts/internal_protos.sh b/ee/ephemeral_environments/scripts/internal_protos.sh new file mode 100755 index 000000000..8304b14f9 --- /dev/null +++ b/ee/ephemeral_environments/scripts/internal_protos.sh @@ -0,0 +1,9 @@ +list=' +ephemeral_environments +rbac +' + +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 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 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..4ff6e63e1 --- /dev/null +++ b/ee/ephemeral_environments/test/grpc/ephemeral_environments_server_test.exs @@ -0,0 +1,393 @@ +defmodule EphemeralEnvironments.Grpc.EphemeralEnvironmentsServerTest do + use ExUnit.Case, async: false + + alias EphemeralEnvironments.Repo + alias EphemeralEnvironments.Repo.EphemeralEnvironmentType, as: Schema + alias Support.Factories + + alias InternalApi.EphemeralEnvironments.{ + CreateRequest, + EphemeralEnvironmentType, + EphemeralEnvironments, + ListRequest, + DescribeRequest, + UpdateRequest + } + + @org_id Ecto.UUID.generate() + @user_id Ecto.UUID.generate() + @grpc_port 50_051 + + 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()}) + + {:ok, channel} = GRPC.Stub.connect("localhost:#{@grpc_port}") + {:ok, channel: channel} + 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, _} = 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()) + + 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, _} = 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) + + # 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 + 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 + 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)) + 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, _} = 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, %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 + + test "allows same name in different orgs", %{channel: channel} do + {: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 + } + } + + assert {:ok, _} = EphemeralEnvironments.Stub.create(channel, request) + end + 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 + request = %UpdateRequest{ + environment_type: %EphemeralEnvironmentType{ + id: Ecto.UUID.generate(), + org_id: @org_id, + name: "Updated Name", + last_updated_by: @user_id + } + } + + {: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: "1") + 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 + } + } + + {: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, _} = 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: "1", + last_updated_by: @user_id + } + } + + {: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, _} = 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: org2_id, + name: "1", + last_updated_by: @user_id + } + } + + assert {:ok, response} = EphemeralEnvironments.Stub.update(channel, request) + 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) + + 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 + assert_recent_timestamp(DateTime.from_unix!(response.environment_type.updated_at.seconds)) + end + 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