Skip to content

Commit

Permalink
Add ability to use templated global services (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljguarino authored Mar 27, 2024
1 parent f4aad5f commit 5d1681e
Show file tree
Hide file tree
Showing 13 changed files with 174 additions and 38 deletions.
12 changes: 12 additions & 0 deletions assets/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5919,8 +5919,14 @@ export type ServiceTemplate = {
helm?: Maybe<HelmSpec>;
/** settings for service kustomization */
kustomize?: Maybe<Kustomize>;
/** the name for this service (optional for managed namespaces) */
name?: Maybe<Scalars['String']['output']>;
/** the namespace for this service (optional for managed namespaces) */
namespace?: Maybe<Scalars['String']['output']>;
/** the id of a repository to source manifests for this service */
repositoryId?: Maybe<Scalars['ID']['output']>;
/** specification of how the templated service will be synced */
syncConfig?: Maybe<SyncConfig>;
templated?: Maybe<Scalars['Boolean']['output']>;
};

Expand All @@ -5934,8 +5940,14 @@ export type ServiceTemplateAttributes = {
helm?: InputMaybe<HelmConfigAttributes>;
/** settings for service kustomization */
kustomize?: InputMaybe<KustomizeAttributes>;
/** the name for this service (optional for managed namespaces) */
name?: InputMaybe<Scalars['String']['input']>;
/** the namespace for this service (optional for managed namespaces) */
namespace?: InputMaybe<Scalars['String']['input']>;
/** the id of a repository to source manifests for this service */
repositoryId?: InputMaybe<Scalars['ID']['input']>;
/** attributes to configure sync settings for this service */
syncConfig?: InputMaybe<SyncConfigAttributes>;
templated?: InputMaybe<Scalars['Boolean']['input']>;
};

Expand Down
8 changes: 8 additions & 0 deletions lib/console.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ defmodule Console do
end
def mapify(v), do: v

def remove_ids(%{id: _} = map) do
Map.delete(map, :id)
|> remove_ids()
end
def remove_ids(%{} = map), do: Map.new(map, fn {k, v} -> {k, remove_ids(v)} end)
def remove_ids(l) when is_list(l), do: Enum.map(l, &remove_ids/1)
def remove_ids(v), do: v

def string_map(%{} = map) do
Poison.encode!(map)
|> Poison.decode!()
Expand Down
1 change: 1 addition & 0 deletions lib/console/deployments/cron.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ defmodule Console.Deployments.Cron do
Logger.info "backfilling global services into all clusters"

GlobalService.stream()
|> GlobalService.preloaded()
|> Repo.stream(method: :keyset)
|> Stream.each(fn global ->
Logger.info "syncing global service #{global.id}"
Expand Down
76 changes: 49 additions & 27 deletions lib/console/deployments/global.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ defmodule Console.Deployments.Global do
alias Console.PubSub
alias Console.Deployments.Services
alias Console.Services.Users
alias Console.Schema.{GlobalService, Service, Cluster, User, Tag, ManagedNamespace, NamespaceInstance}
alias Console.Schema.{
GlobalService,
Service,
Cluster,
User,
Tag,
ManagedNamespace,
NamespaceInstance,
ServiceTemplate
}
require Logger

@type global_resp :: {:ok, GlobalService.t} | Console.error
Expand Down Expand Up @@ -32,6 +41,7 @@ defmodule Console.Deployments.Global do
"""
def update(attrs, id, %User{} = user) do
get!(id)
|> Repo.preload([:template])
|> GlobalService.changeset(attrs)
|> allow(user, :write)
|> when_ok(:update)
Expand Down Expand Up @@ -129,10 +139,18 @@ defmodule Console.Deployments.Global do
Clones the global service directly into the target cluster
"""
@spec add_to_cluster(GlobalService.t, Cluster.t) :: Services.service_resp
def add_to_cluster(%GlobalService{id: gid, service_id: sid}, %Cluster{id: cid}) do
Services.clone_service(%{owner_id: gid}, sid, cid, bot())
def add_to_cluster(global, cluster), do: add_to_cluster(global, cluster, bot())

@spec add_to_cluster(GlobalService.t, Cluster.t, User.t) :: Services.service_resp
def add_to_cluster(%GlobalService{id: gid, template: %ServiceTemplate{} = tpl}, %Cluster{id: id}, user) do
ServiceTemplate.attributes(tpl)
|> Map.put(:owner_id, gid)
|> Services.create_service(id, user)
end

def add_to_cluster(%GlobalService{id: gid, service_id: sid}, %Cluster{id: cid}, user),
do: Services.clone_service(%{owner_id: gid}, sid, cid, user)

@doc """
Ensures a managed namespace is synchronized across all target clusters
"""
Expand Down Expand Up @@ -185,11 +203,8 @@ defmodule Console.Deployments.Global do
end
defp create_namespace_instance(ns, _, _), do: Logger.info "Namespace #{ns.name}[#{ns.id}] does not specify a service"

defp namespace_service_attrs(%ManagedNamespace{service: %{}} = ns) do
Console.mapify(ns.service)
|> Map.put(:context_bindings, Enum.map(ns.service.contexts || [], & %{context_id: &1}))
|> Map.put(:namespace, ns.name)
|> Map.put(:name, "#{ns.name}-core")
defp namespace_service_attrs(%ManagedNamespace{service: %{} = tpl} = ns) do
ServiceTemplate.attributes(tpl, ns.name, "#{ns.name}-core")
|> Map.put(:sync_config, %{create_namespace: false})
end

Expand Down Expand Up @@ -222,16 +237,16 @@ defmodule Console.Deployments.Global do
"""
@spec sync_clusters(GlobalService.t) :: :ok
def sync_clusters(%GlobalService{id: gid} = global) do
%{service: svc} = Repo.preload(global, [:service])
%{service: svc} = global = Repo.preload(global, [:service, :template])
bot = bot()
Cluster.ignore_ids([svc.cluster_id])
Cluster.ignore_ids(if not is_nil(svc), do: [svc.cluster_id], else: [])
|> Cluster.target(global)
|> Repo.all()
|> Enum.each(fn %{id: cluster_id} ->
|> Enum.each(fn %{id: cluster_id} = cluster ->
case Services.get_service_by_name(cluster_id, svc.name) do
%Service{owner_id: ^gid} = dest -> sync_service(svc, dest, bot)
%Service{owner_id: ^gid} = dest -> sync_service(global, dest, bot)
%Service{} -> :ok # ignore if the service was created out of band
nil -> Services.clone_service(%{owner_id: gid}, svc.id, cluster_id, bot)
nil -> add_to_cluster(global, cluster, bot)
end
end)
end
Expand All @@ -241,7 +256,18 @@ defmodule Console.Deployments.Global do
@doc """
it can resync a service owned by a global service
"""
@spec sync_service(Service.t, Service.t, User.t) :: Services.service_resp | :ok
@spec sync_service(GlobalService.t | Service.t, Service.t, User.t) :: Services.service_resp | :ok
def sync_service(%GlobalService{template: %ServiceTemplate{} = tpl}, %Service{} = dest, %User{} = user) do
Logger.info "Attempting to resync service #{dest.id}"
case diff?(tpl, dest) do
true -> ServiceTemplate.attributes(tpl) |> Services.update_service(dest.id, user)
false -> Logger.info "did not update service due to no differences"
end
end

def sync_service(%GlobalService{service: %Service{} = source}, %Service{} = dest, %User{} = user),
do: sync_service(source, dest, user)

def sync_service(%Service{} = source, %Service{} = dest, %User{} = user) do
Logger.info "attempting to resync service #{dest.id}"
with {:ok, source_secrets} <- Services.configuration(source),
Expand All @@ -263,8 +289,11 @@ defmodule Console.Deployments.Global do
@doc """
Determines if services are different enough to merit resyncing
"""
@spec diff?(Service.t | ManagedNamespace.ServiceSpec.t, Service.t) :: boolean | {:error, term}
def diff?(%ManagedNamespace.ServiceSpec{} = spec, %Service{} = dest) do
@spec diff?(Service.t | ManagedNamespace.t | ServiceTemplate.t, Service.t) :: boolean | {:error, term}
def diff?(%ManagedNamespace{service: %ServiceTemplate{} = template}, %Service{} = dest),
do: diff?(template, dest)

def diff?(%ServiceTemplate{} = spec, %Service{} = dest) do
spec.repository_id != dest.repository_id || spec.templated != dest.templated || specs_different?(spec, dest) || contexts_different?(spec, dest)
end

Expand All @@ -291,8 +320,9 @@ defmodule Console.Deployments.Global do
Enum.any?(source, fn {k, v} -> dest[k] != v end)
end

defp contexts_different?(%{contexts: ctxs}, svc) do
Enum.map(svc.context_bindings, & &1.context_id) == (ctxs || [])
defp contexts_different?(%ServiceTemplate{contexts: ctxs}, svc) do
MapSet.new(svc.context_bindings, & &1.context_id)
|> MapSet.equal?(MapSet.new(ctxs || []))
end

defp specs_different?(source, dest) do
Expand All @@ -305,16 +335,8 @@ defmodule Console.Deployments.Global do

defp clean(val) do
Console.mapify(val)
|> remove_ids()
end

defp remove_ids(%{id: _} = map) do
Map.delete(map, :id)
|> remove_ids()
|> Console.remove_ids()
end
defp remove_ids(%{} = map), do: Map.new(map, fn {k, v} -> {k, remove_ids(v)} end)
defp remove_ids(l) when is_list(l), do: Enum.map(l, &remove_ids/1)
defp remove_ids(v), do: v

def notify({:ok, %GlobalService{} = svc}, :create, user),
do: handle_notify(PubSub.GlobalServiceCreated, svc, actor: user)
Expand Down
1 change: 1 addition & 0 deletions lib/console/deployments/pubsub/recurse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ defimpl Console.PubSub.Recurse, for: [Console.PubSub.ClusterCreated, Console.Pub
cluster = Repo.preload(cluster, [:tags])
bot = %{Users.get_bot!("console") | roles: %{admin: true}}
GlobalService.stream()
|> GlobalService.preloaded()
|> Repo.stream(method: :keyset)
|> Stream.filter(&Global.match?(&1, cluster))
|> Stream.each(&Global.add_to_cluster(&1, cluster))
Expand Down
18 changes: 12 additions & 6 deletions lib/console/graphql/deployments/global.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ defmodule Console.GraphQl.Deployments.Global do

@desc "Attributes for configuring a service in something like a managed namespace"
input_object :service_template_attributes do
field :name, :string, description: "the name for this service (optional for managed namespaces)"
field :namespace, :string, description: "the namespace for this service (optional for managed namespaces)"
field :templated, :boolean
field :repository_id, :id, description: "the id of a repository to source manifests for this service"
field :contexts, list_of(:id), description: "a list of context ids to add to this service"

field :git, :git_ref_attributes, description: "settings to configure git for a service"
field :helm, :helm_config_attributes, description: "settings to configure helm for a service"
field :kustomize, :kustomize_attributes, description: "settings for service kustomization"
field :git, :git_ref_attributes, description: "settings to configure git for a service"
field :helm, :helm_config_attributes, description: "settings to configure helm for a service"
field :kustomize, :kustomize_attributes, description: "settings for service kustomization"
field :sync_config, :sync_config_attributes, description: "attributes to configure sync settings for this service"
end

@desc "A spec for targeting clusters"
Expand Down Expand Up @@ -76,13 +79,16 @@ defmodule Console.GraphQl.Deployments.Global do

@desc "Attributes for configuring a service in something like a managed namespace"
object :service_template do
field :name, :string, description: "the name for this service (optional for managed namespaces)"
field :namespace, :string, description: "the namespace for this service (optional for managed namespaces)"
field :templated, :boolean
field :repository_id, :id, description: "the id of a repository to source manifests for this service"
field :contexts, list_of(:id), description: "a list of context ids to add to this service"

field :git, :git_ref, description: "settings to configure git for a service"
field :helm, :helm_spec, description: "settings to configure helm for a service"
field :kustomize, :kustomize, description: "settings for service kustomization"
field :git, :git_ref, description: "settings to configure git for a service"
field :helm, :helm_spec, description: "settings to configure helm for a service"
field :kustomize, :kustomize, description: "settings for service kustomization"
field :sync_config, :sync_config, description: "specification of how the templated service will be synced"
end

@desc "A spec for targeting clusters"
Expand Down
8 changes: 7 additions & 1 deletion lib/console/schema/global_service.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Console.Schema.GlobalService do
use Piazza.Ecto.Schema
alias Console.Schema.{Service, Cluster, ClusterProvider}
alias Console.Schema.{Service, Cluster, ClusterProvider, ServiceTemplate}

schema "global_services" do
field :name, :string
Expand All @@ -11,6 +11,7 @@ defmodule Console.Schema.GlobalService do
field :value, :string
end

belongs_to :template, ServiceTemplate
belongs_to :service, Service
belongs_to :provider, ClusterProvider

Expand All @@ -21,13 +22,18 @@ defmodule Console.Schema.GlobalService do
from(g in query, order_by: ^order)
end

def preloaded(query \\ __MODULE__, preloads \\ [:template, :service]) do
from(g in query, preload: ^preloads)
end

def stream(query \\ __MODULE__), do: ordered(query, asc: :id)

@valid ~w(name service_id distro provider_id)a

def changeset(model, attrs \\ %{}) do
model
|> cast(attrs, @valid)
|> cast_assoc(:template)
|> cast_embed(:tags, with: &tag_changeset/2)
|> unique_constraint(:service_id)
|> unique_constraint(:name)
Expand Down
25 changes: 23 additions & 2 deletions lib/console/schema/service_template.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
defmodule Console.Schema.ServiceTemplate do
use Piazza.Ecto.Schema
alias Console.Schema.{GitRepository, Service}
alias Console.Schema.{GitRepository, Service, Metadata}

schema "service_templates" do
field :name, :string
field :namespace, :string
field :templated, :boolean, default: true
field :contexts, {:array, :string}

Expand All @@ -13,12 +15,30 @@ defmodule Console.Schema.ServiceTemplate do
field :path, :string
end

embeds_one :sync_config, SyncConfig, on_replace: :update do
embeds_one :namespace_metadata, Metadata
field :create_namespace, :boolean, default: true
end

belongs_to :repository, GitRepository

timestamps()
end

@valid ~w(templated repository_id contexts)a

def attributes(%__MODULE__{} = tpl) do
Map.new(__schema__(:fields) -- [:id], & {&1, Map.get(tpl, &1) |> Console.mapify()})
|> Console.remove_ids()
|> Map.put(:context_bindings, Enum.map(tpl.contexts || [], & %{context_id: &1}))
end

def attributes(%__MODULE__{} = tpl, namespace, name) do
attributes(tpl)
|> Map.put(:namespace, namespace)
|> Map.put(:name, name)
end

@valid ~w(name namespace templated repository_id contexts)a

def changeset(model, attrs \\ %{}) do
model
Expand All @@ -27,5 +47,6 @@ defmodule Console.Schema.ServiceTemplate do
|> cast_embed(:git)
|> cast_embed(:helm)
|> cast_embed(:kustomize, with: &Service.kustomize_changeset/2)
|> cast_embed(:sync_config, with: &Service.sync_config_changeset/2)
end
end
15 changes: 15 additions & 0 deletions priv/repo/migrations/20240327041928_add_global_template.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Console.Repo.Migrations.AddGlobalTemplate do
use Ecto.Migration

def change do
alter table(:global_services) do
add :template_id, references(:service_templates, type: :uuid, on_delete: :delete_all)
end

alter table(:service_templates) do
add :sync_config, :map
add :namespace, :string
add :name, :string
end
end
end
18 changes: 18 additions & 0 deletions schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,12 @@ input ManagedNamespaceAttributes {

"Attributes for configuring a service in something like a managed namespace"
input ServiceTemplateAttributes {
"the name for this service (optional for managed namespaces)"
name: String

"the namespace for this service (optional for managed namespaces)"
namespace: String

templated: Boolean

"the id of a repository to source manifests for this service"
Expand All @@ -863,6 +869,9 @@ input ServiceTemplateAttributes {

"settings for service kustomization"
kustomize: KustomizeAttributes

"attributes to configure sync settings for this service"
syncConfig: SyncConfigAttributes
}

"A spec for targeting clusters"
Expand Down Expand Up @@ -936,6 +945,12 @@ type ManagedNamespace {

"Attributes for configuring a service in something like a managed namespace"
type ServiceTemplate {
"the name for this service (optional for managed namespaces)"
name: String

"the namespace for this service (optional for managed namespaces)"
namespace: String

templated: Boolean

"the id of a repository to source manifests for this service"
Expand All @@ -952,6 +967,9 @@ type ServiceTemplate {

"settings for service kustomization"
kustomize: Kustomize

"specification of how the templated service will be synced"
syncConfig: SyncConfig
}

"A spec for targeting clusters"
Expand Down
Loading

0 comments on commit 5d1681e

Please sign in to comment.