core / periphery support ghcr

This commit is contained in:
mbecker20
2024-06-09 02:01:51 -07:00
parent 5c3294241d
commit 568c963419
18 changed files with 613 additions and 198 deletions

View File

@@ -8,7 +8,7 @@ use monitor_client::{
},
entities::{
all_logs_success,
build::Build,
build::{Build, CloudRegistryConfig, ImageRegistry},
builder::{AwsBuilderConfig, Builder, BuilderConfig},
deployment::DeploymentState,
monitor_timestamp,
@@ -72,6 +72,9 @@ impl Resolve<RunBuild, (User, Update)> for State {
)
.await?;
let registry_token =
validate_account_extract_registry_token(&build)?;
// get the action state for the build (or insert default).
let action_state =
action_states().build.get_or_insert_default(&build.id).await;
@@ -181,11 +184,6 @@ impl Resolve<RunBuild, (User, Update)> for State {
update_update(update.clone()).await?;
if all_logs_success(&update.logs) {
let docker_token = core_config
.docker_accounts
.get(&build.config.docker_account)
.cloned();
// Interpolate variables / secrets into build args
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
@@ -237,7 +235,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
res = periphery
.request(api::build::Build {
build: build.clone(),
docker_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
}) => res.context("failed at call to periphery to build"),
_ = cancel.cancelled() => {
@@ -282,6 +280,7 @@ impl Resolve<RunBuild, (User, Update)> for State {
.await;
}
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
@@ -675,3 +674,35 @@ fn start_aws_builder_log(
format!("instance id: {instance_id}\nip: {ip}\nami id: {ami_id}\ninstance type: {instance_type}\nvolume size: {volume_gb} GB\nsubnet id: {subnet_id}\nsecurity groups: {readable_sec_group_ids}\nassign public ip: {assign_public_ip}\nuse public ip: {use_public_ip}")
}
/// This will make sure that a build with non-none image registry has an account attached,
/// and will check the core config for a token matching requirements (otherwise it is left to periphery)
fn validate_account_extract_registry_token(
build: &Build,
) -> anyhow::Result<Option<String>> {
match &build.config.image_registry {
ImageRegistry::None(_) => Ok(None),
ImageRegistry::DockerHub(CloudRegistryConfig {
account, ..
}) => {
if account.is_empty() {
return Err(anyhow!(
"Must attach account to use DockerHub image registry"
));
}
Ok(core_config().docker_accounts.get(account).cloned())
}
ImageRegistry::GithubContainerRegistry(CloudRegistryConfig {
account,
..
}) => {
if account.is_empty() {
return Err(anyhow!(
"Must attach account to use GithubContainerRegistry"
));
}
Ok(core_config().github_accounts.get(account).cloned())
}
ImageRegistry::Custom(_) => todo!(),
}
}

View File

@@ -5,7 +5,7 @@ use futures::future::join_all;
use monitor_client::{
api::execute::*,
entities::{
build::Build,
build::{Build, ImageRegistry},
deployment::{Deployment, DeploymentImage},
get_image_name,
permission::PermissionLevel,
@@ -78,6 +78,8 @@ impl Resolve<Deploy, (User, Update)> for State {
let periphery = periphery_client(&server)?;
// This block gets the version of the image to deploy in the Build case.
// It also gets the name of the image from the build and attaches it directly.
let version = match deployment.config.image {
DeploymentImage::Build { build_id, version } => {
let build = resource::get::<Build>(&build_id).await?;
@@ -91,10 +93,13 @@ impl Resolve<Deploy, (User, Update)> for State {
deployment.config.image = DeploymentImage::Image {
image: format!("{image_name}:{version}"),
};
// set docker account to match build docker account if it's not overridden by deployment
if deployment.config.docker_account.is_empty() {
deployment.config.docker_account =
build.config.docker_account;
// set image registry to match build docker account if it's not overridden by deployment
if matches!(
&deployment.config.image_registry,
ImageRegistry::None(_)
) {
deployment.config.image_registry =
build.config.image_registry;
}
version
}
@@ -156,17 +161,25 @@ impl Resolve<Deploy, (User, Update)> for State {
update.version = version;
update_update(update.clone()).await?;
let docker_token = core_config
.docker_accounts
.get(&deployment.config.docker_account)
.cloned();
let registry_token = match &deployment.config.image_registry {
ImageRegistry::None(_) => None,
ImageRegistry::DockerHub(params) => {
core_config.docker_accounts.get(&params.account).cloned()
}
ImageRegistry::GithubContainerRegistry(params) => {
core_config.github_accounts.get(&params.account).cloned()
}
ImageRegistry::Custom(_) => {
return Err(anyhow!("Custom ImageRegistry not yet supported"))
}
};
match periphery
.request(api::container::Deploy {
deployment,
stop_signal,
stop_time,
docker_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
})
.await

View File

@@ -1,5 +1,7 @@
use anyhow::{anyhow, Context};
use monitor_client::entities::build::{BuildConfig, BuildInfo};
use monitor_client::entities::build::{
BuildConfig, BuildInfo, CloudRegistryConfig, ImageRegistry,
};
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
use serde::{Deserialize, Serialize};
@@ -213,10 +215,14 @@ impl TryFrom<Build> for monitor_client::entities::build::Build {
repo: value.repo.unwrap_or_default(),
branch: value.branch.unwrap_or_default(),
github_account: value.github_account.unwrap_or_default(),
docker_account: value.docker_account.unwrap_or_default(),
docker_organization: value
.docker_organization
.unwrap_or_default(),
image_registry: ImageRegistry::DockerHub(
CloudRegistryConfig {
account: value.docker_account.unwrap_or_default(),
organization: value
.docker_organization
.unwrap_or_default(),
},
),
pre_build: value
.pre_build
.map(|command| monitor_client::entities::SystemCommand {

View File

@@ -1,3 +1,7 @@
use monitor_client::entities::{
build::{CloudRegistryConfig, ImageRegistry},
NoData,
};
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
use serde::{Deserialize, Serialize};
@@ -374,10 +378,15 @@ impl TryFrom<Deployment>
.post_image
.unwrap_or_default(),
extra_args: value.docker_run_args.extra_args,
docker_account: value
.docker_run_args
.docker_account
.unwrap_or_default(),
image_registry: match value.docker_run_args.docker_account {
Some(account) => {
ImageRegistry::DockerHub(CloudRegistryConfig {
account,
..Default::default()
})
}
None => ImageRegistry::None(NoData {}),
},
labels: Default::default(),
},
};

View File

@@ -0,0 +1,299 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::{resource::Resource, EnvironmentVar, Version};
pub type Deployment = Resource<DeploymentConfig, ()>;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeploymentConfig {
/// The id of server the deployment is deployed on.
#[serde(default, alias = "server")]
pub server_id: String,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
pub send_alerts: bool,
/// The image which the deployment deploys.
/// Can either be a user inputted image, or a Monitor build.
#[serde(default)]
pub image: DeploymentImage,
/// Override the account used to pull the image from the registry.
/// If it is empty, will use the same account as the build.
#[serde(default)]
pub docker_account: String,
/// Whether to skip secret interpolation into the deployment environment variables.
#[serde(default)]
pub skip_secret_interp: bool,
/// Whether to redeploy the deployment whenever the attached build finishes.
#[serde(default)]
pub redeploy_on_build: bool,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(default = "default_term_signal_labels")]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
pub termination_timeout: i32,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default)]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default)]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(default)]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(default)]
pub labels: Vec<EnvironmentVar>,
/// The network attached to the container.
/// Default is `host`.
#[serde(default = "default_network")]
pub network: String,
/// The restart mode given to the container.
#[serde(default)]
pub restart: RestartMode,
/// This is interpolated at the end of the `docker run` command,
/// which means they are either passed to the containers inner process,
/// or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.
/// Empty is no command.
#[serde(default)]
pub command: String,
/// Extra args which are interpolated into the `docker run` command,
/// and affect the container configuration.
#[serde(default)]
pub extra_args: Vec<String>,
}
fn default_send_alerts() -> bool {
true
}
fn default_term_signal_labels() -> Vec<TerminationSignalLabel> {
vec![TerminationSignalLabel::default()]
}
fn default_termination_timeout() -> i32 {
10
}
fn default_network() -> String {
String::from("host")
}
impl Default for DeploymentConfig {
fn default() -> Self {
Self {
server_id: Default::default(),
send_alerts: default_send_alerts(),
image: Default::default(),
docker_account: Default::default(),
skip_secret_interp: Default::default(),
redeploy_on_build: Default::default(),
term_signal_labels: default_term_signal_labels(),
termination_signal: Default::default(),
termination_timeout: default_termination_timeout(),
ports: Default::default(),
volumes: Default::default(),
environment: Default::default(),
labels: Default::default(),
network: default_network(),
restart: Default::default(),
command: Default::default(),
extra_args: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(tag = "type", content = "params")]
pub enum DeploymentImage {
/// Deploy any external image.
Image {
/// The docker image, can be from any registry that works with docker and that the host server can reach.
#[serde(default)]
image: String,
},
/// Deploy a monitor build.
Build {
/// The id of the build
#[serde(default, alias = "build")]
build_id: String,
/// Use a custom / older version of the image produced by the build.
/// if version is 0.0.0, this means `latest` image.
#[serde(default)]
version: Version,
},
}
impl Default for DeploymentImage {
fn default() -> Self {
Self::Image {
image: Default::default(),
}
}
}
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,
)]
pub struct Conversion {
/// reference on the server.
pub local: String,
/// reference in the container.
pub container: String,
}
/// A summary of a docker container on a server.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ContainerSummary {
/// Name of the container.
pub name: String,
/// Id of the container.
pub id: String,
/// The image the container is based on.
pub image: String,
/// The docker labels on the container.
pub labels: HashMap<String, String>,
/// The state of the container, like `running` or `not_deployed`
pub state: DeploymentState,
/// The status string of the docker container.
pub status: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DockerContainerStats {
#[serde(alias = "Name")]
pub name: String,
#[serde(alias = "CPUPerc")]
pub cpu_perc: String,
#[serde(alias = "MemPerc")]
pub mem_perc: String,
#[serde(alias = "MemUsage")]
pub mem_usage: String,
#[serde(alias = "NetIO")]
pub net_io: String,
#[serde(alias = "BlockIO")]
pub block_io: String,
#[serde(alias = "PIDs")]
pub pids: String,
}
/// Variants de/serialized from/to snake_case.
///
/// Eg.
/// - NotDeployed -> not_deployed
/// - Restarting -> restarting
/// - Running -> running.
#[derive(
Serialize,
Deserialize,
Debug,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Default,
)]
#[serde(rename_all = "snake_case")]
pub enum DeploymentState {
#[default]
Unknown,
NotDeployed,
Created,
Restarting,
Running,
Removing,
Paused,
Exited,
Dead,
}
#[derive(
Serialize,
Deserialize,
Debug,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Default,
)]
pub enum RestartMode {
#[default]
#[serde(rename = "no")]
NoRestart,
#[serde(rename = "on-failure")]
OnFailure,
#[serde(rename = "always")]
Always,
#[serde(rename = "unless-stopped")]
UnlessStopped,
}
#[derive(
Serialize,
Deserialize,
Debug,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Default,
)]
#[serde(rename_all = "UPPERCASE")]
pub enum TerminationSignal {
#[serde(alias = "1")]
SigHup,
#[serde(alias = "2")]
SigInt,
#[serde(alias = "3")]
SigQuit,
#[default]
#[serde(alias = "15")]
SigTerm,
}
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq,
)]
pub struct TerminationSignalLabel {
pub signal: TerminationSignal,
pub label: String,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct DeploymentActionState {
pub deploying: bool,
pub stopping: bool,
pub starting: bool,
pub removing: bool,
pub renaming: bool,
}

View File

@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
pub mod build;
pub mod resource;
pub mod deployment;
#[derive(
Serialize, Deserialize, Debug, Clone, Default, PartialEq,

View File

@@ -17,12 +17,12 @@ impl Resolve<Build> for State {
&self,
Build {
build,
docker_token,
registry_token,
replacers,
}: Build,
_: (),
) -> anyhow::Result<Vec<Log>> {
docker::build::build(&build, docker_token, replacers).await
docker::build::build(&build, registry_token, replacers).await
}
}

View File

@@ -192,9 +192,9 @@ impl Resolve<Deploy> for State {
&self,
Deploy {
deployment,
docker_token,
stop_signal,
stop_time,
registry_token,
replacers,
}: Deploy,
_: (),
@@ -207,7 +207,7 @@ impl Resolve<Deploy> for State {
stop_time
.unwrap_or(deployment.config.termination_timeout)
.into(),
docker_token,
registry_token,
replacers,
)
.await;

View File

@@ -1,4 +1,4 @@
use anyhow::anyhow;
use anyhow::{anyhow, Context};
use monitor_client::entities::{
to_monitor_name, update::Log, CloneArgs, LatestCommit,
};
@@ -35,12 +35,17 @@ impl Resolve<CloneRepo> for State {
_: (),
) -> anyhow::Result<Vec<Log>> {
let CloneArgs { github_account, .. } = &args;
let github_token =
match (github_token, get_github_token(github_account)) {
(Some(token), _) => Some(token),
(None, Ok(token)) => token,
(None, Err(e)) => return Err(e),
};
let github_token = match (github_account, github_token) {
(None, _) => None,
(Some(_), Some(token)) => Some(token),
(Some(account), None) => Some(
get_github_token(account)
.context(
"failed to get github token from periphery config",
)?
.clone(),
),
};
git::clone(args, &periphery_config().repo_dir, github_token).await
}
}

View File

@@ -2,13 +2,13 @@ use anyhow::Context;
use command::run_monitor_command;
use monitor_client::entities::{
build::{Build, BuildConfig},
optional_string, to_monitor_name,
get_image_name, optional_string, to_monitor_name,
update::Log,
EnvironmentVar, Version,
};
use serror::serialize_error_pretty;
use crate::{config::periphery_config, helpers::get_docker_token};
use crate::config::periphery_config;
use super::{docker_login, parse_extra_args, parse_labels};
@@ -18,15 +18,17 @@ pub async fn prune_images() -> Log {
run_monitor_command("prune images", command).await
}
#[instrument(skip(docker_token, core_replacers))]
#[instrument(skip(registry_token, core_replacers))]
pub async fn build(
Build {
build: &Build,
registry_token: Option<String>,
core_replacers: Vec<(String, String)>,
) -> anyhow::Result<Vec<Log>> {
let Build {
name,
config:
BuildConfig {
version,
docker_account,
docker_organization,
skip_secret_interp,
build_path,
dockerfile_path,
@@ -34,61 +36,66 @@ pub async fn build(
labels,
extra_args,
use_buildx,
image_registry,
..
},
..
}: &Build,
docker_token: Option<String>,
core_replacers: Vec<(String, String)>,
) -> anyhow::Result<Vec<Log>> {
} = build;
let mut logs = Vec::new();
let docker_token = match (
docker_token,
get_docker_token(&optional_string(docker_account)),
) {
(Some(docker_token), _) => Some(docker_token),
(None, Ok(docker_token)) => docker_token,
(None, Err(e)) => {
logs.push(Log::error("build", serialize_error_pretty(&e)));
// Maybe docker login
let should_push = match docker_login(
image_registry,
registry_token.as_deref(),
)
.await
{
Ok(should_push) => should_push,
Err(e) => {
logs.push(Log::error(
"docker login",
serialize_error_pretty(
&e.context("failed to login to docker registry"),
),
));
return Ok(logs);
}
};
// Get paths
let name = to_monitor_name(name);
let using_account =
docker_login(&optional_string(docker_account), &docker_token)
.await
.context("failed to login to docker")?;
let build_dir =
periphery_config().repo_dir.join(&name).join(build_path);
let dockerfile_path = match optional_string(dockerfile_path) {
Some(dockerfile_path) => dockerfile_path.to_owned(),
None => "Dockerfile".to_owned(),
};
// Get command parts
let build_args = parse_build_args(build_args);
let labels = parse_labels(labels);
let extra_args = parse_extra_args(extra_args);
let buildx = if *use_buildx { " buildx" } else { "" };
let image_name = get_image_name(
&name,
&optional_string(docker_account),
&optional_string(docker_organization),
);
let image_name = get_image_name(build);
let image_tags = image_tags(&image_name, version);
let docker_push = if using_account {
format!(" && docker image push --all-tags {image_name}")
} else {
String::new()
};
let push_command = should_push
.then(|| format!(" && docker image push --all-tags {image_name}"))
.unwrap_or_default();
// Construct command
let command = format!(
"cd {} && docker{buildx} build{build_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{docker_push}",
"cd {} && docker{buildx} build{build_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
build_dir.display()
);
if *skip_secret_interp {
let build_log =
run_monitor_command("docker build", command).await;
info!("finished building docker image");
logs.push(build_log);
} else {
// Interpolate any missing secrets
let (command, mut replacers) = svi::interpolate_variables(
&command,
&periphery_config().secrets,
@@ -110,23 +117,8 @@ pub async fn build(
svi::replace_in_string(&build_log.stderr, &replacers);
logs.push(build_log);
}
Ok(logs)
}
fn get_image_name(
name: &str,
docker_account: &Option<String>,
docker_organization: &Option<String>,
) -> String {
match docker_organization {
Some(docker_org) => format!("{docker_org}/{name}"),
None => match docker_account {
Some(docker_account) => {
format!("{docker_account}/{name}")
}
None => name.to_string(),
},
}
Ok(logs)
}
fn image_tags(image_name: &str, version: &Version) -> String {

View File

@@ -5,14 +5,14 @@ use monitor_client::entities::{
Conversion, Deployment, DeploymentConfig, DeploymentImage,
DockerContainerStats, RestartMode, TerminationSignal,
},
optional_string, to_monitor_name,
to_monitor_name,
update::Log,
EnvironmentVar, SearchCombinator,
};
use run_command::async_run_command;
use serror::serialize_error_pretty;
use crate::{config::periphery_config, helpers::get_docker_token};
use crate::config::periphery_config;
use super::{docker_login, parse_extra_args, parse_labels};
@@ -173,34 +173,26 @@ async fn pull_image(image: &str) -> Log {
run_monitor_command("docker pull", command).await
}
#[instrument(skip(docker_token, core_replacers))]
#[instrument(skip(registry_token, core_replacers))]
pub async fn deploy(
deployment: &Deployment,
stop_signal: Option<TerminationSignal>,
stop_time: Option<i32>,
docker_token: Option<String>,
registry_token: Option<String>,
core_replacers: Vec<(String, String)>,
) -> Log {
let docker_token = match (
docker_token,
get_docker_token(&optional_string(
&deployment.config.docker_account,
)),
) {
(Some(token), _) => Some(token),
(None, Ok(token)) => token,
(None, Err(e)) => {
return Log::error("docker login", serialize_error_pretty(&e))
}
};
if let Err(e) = docker_login(
&optional_string(&deployment.config.docker_account),
&docker_token,
&deployment.config.image_registry,
registry_token.as_deref(),
)
.await
{
return Log::error("docker login", serialize_error_pretty(&e));
return Log::error(
"docker login",
serialize_error_pretty(
&e.context("failed to login to docker registry"),
),
);
}
let image = if let DeploymentImage::Image { image } =

View File

@@ -1,40 +1,65 @@
use anyhow::anyhow;
use command::run_monitor_command;
use monitor_client::entities::{update::Log, EnvironmentVar};
use monitor_client::entities::{
build::{CloudRegistryConfig, ImageRegistry},
update::Log,
EnvironmentVar,
};
use run_command::async_run_command;
use crate::helpers::{get_docker_token, get_github_token};
pub mod build;
pub mod client;
pub mod container;
pub mod network;
pub fn get_docker_username_pw(
docker_account: &Option<String>,
docker_token: &Option<String>,
) -> anyhow::Result<Option<(String, String)>> {
match docker_account {
Some(docker_account) => match docker_token {
Some(docker_token) => Ok(Some((docker_account.to_owned(), docker_token.to_owned()))),
None => Err(anyhow!(
"docker token for account {docker_account} has not been configured on this client"
)),
},
None => Ok(None),
}
}
/// Returns whether build result should be pushed after build
pub async fn docker_login(
docker_account: &Option<String>,
docker_token: &Option<String>,
registry: &ImageRegistry,
// For local token override from core.
registry_token: Option<&str>,
) -> anyhow::Result<bool> {
let docker_account_u_pw =
get_docker_username_pw(docker_account, docker_token)?;
if let Some((username, password)) = &docker_account_u_pw {
let login = format!("docker login -u {username} -p {password}");
async_run_command(&login).await;
Ok(true)
} else {
Ok(false)
match registry {
ImageRegistry::None(_) => Ok(false),
ImageRegistry::DockerHub(CloudRegistryConfig {
account, ..
}) => {
if account.is_empty() {
return Err(anyhow!(
"Must configure account for DockerHub registry"
));
}
let registry_token = match registry_token {
Some(token) => token,
None => get_docker_token(account)?,
};
async_run_command(&format!(
"docker login -u {account} -p {registry_token}",
))
.await;
Ok(true)
}
ImageRegistry::GithubContainerRegistry(CloudRegistryConfig {
account,
..
}) => {
if account.is_empty() {
return Err(anyhow!(
"Must configure account for GithubContainerRegistry"
));
}
let registry_token = match registry_token {
Some(token) => token,
None => get_github_token(account)?,
};
async_run_command(&format!(
"docker login ghcr.io -u {account} -p {registry_token}",
))
.await;
Ok(true)
}
ImageRegistry::Custom(_) => todo!(),
}
}

View File

@@ -1,35 +1,21 @@
use anyhow::anyhow;
use anyhow::Context;
use crate::config::periphery_config;
pub fn get_github_token(
github_account: &Option<String>,
) -> anyhow::Result<Option<String>> {
match github_account {
Some(account) => {
match periphery_config().github_accounts.get(account) {
Some(token) => Ok(Some(token.to_owned())),
None => Err(anyhow!(
"did not find token in config for github account {account}"
)),
}
}
None => Ok(None),
}
github_account: &String,
) -> anyhow::Result<&'static String> {
periphery_config()
.github_accounts
.get(github_account)
.with_context(|| format!("did not find token in config for github account {github_account}"))
}
pub fn get_docker_token(
docker_account: &Option<String>,
) -> anyhow::Result<Option<String>> {
match docker_account {
Some(account) => {
match periphery_config().docker_accounts.get(account) {
Some(token) => Ok(Some(token.to_owned())),
None => Err(anyhow!(
"did not find token in config for docker account {account}"
)),
}
}
None => Ok(None),
}
docker_account: &String,
) -> anyhow::Result<&'static String> {
periphery_config()
.docker_accounts
.get(docker_account)
.with_context(|| format!("did not find token in config for docker account {docker_account}"))
}

View File

@@ -10,7 +10,7 @@ use crate::entities::I64;
use super::{
resource::{Resource, ResourceListItem, ResourceQuery},
EnvironmentVar, SystemCommand, Version,
EnvironmentVar, NoData, SystemCommand, Version,
};
#[typeshare]
@@ -103,18 +103,6 @@ pub struct BuildConfig {
#[builder(default)]
pub github_account: String,
/// The dockerhub account used to push the image to dockerhub.
/// Empty string means no dockerhub push (server local build).
#[serde(default)]
#[builder(default)]
pub docker_account: String,
/// The docker organization which the image should be pushed under.
/// Empty string means no organization.
#[serde(default)]
#[builder(default)]
pub docker_organization: String,
/// The optional command run after repo clone and before docker build.
#[serde(default)]
#[builder(default)]
@@ -153,6 +141,11 @@ pub struct BuildConfig {
#[builder(default)]
pub use_buildx: bool,
/// Configuration for the registry to push the built image to.
#[serde(default)]
#[builder(default)]
pub image_registry: ImageRegistry,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
#[builder(default = "default_webhook_enabled()")]
@@ -192,8 +185,6 @@ impl Default for BuildConfig {
branch: default_branch(),
commit: Default::default(),
github_account: Default::default(),
docker_account: Default::default(),
docker_organization: Default::default(),
pre_build: Default::default(),
build_path: default_build_path(),
dockerfile_path: default_dockerfile_path(),
@@ -201,11 +192,48 @@ impl Default for BuildConfig {
labels: Default::default(),
extra_args: Default::default(),
use_buildx: Default::default(),
image_registry: Default::default(),
webhook_enabled: default_webhook_enabled(),
}
}
}
/// Configuration for the registry to push the built image to.
#[typeshare]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ImageRegistry {
/// Don't push the image to any registry
None(NoData),
/// Push the image to DockerHub
DockerHub(CloudRegistryConfig),
/// Push the image to the Github Container Registry
GithubContainerRegistry(CloudRegistryConfig),
/// Todo. Will point to a custom "Registry" resource by id
Custom(String),
}
impl Default for ImageRegistry {
fn default() -> Self {
Self::None(NoData {})
}
}
/// Configuration for a cloud image registry, like account and organization.
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct CloudRegistryConfig {
/// Specify an account to use with the cloud registry.
#[serde(default)]
pub account: String,
/// Optional. Specify an organization to push the image under.
/// Empty string means no organization.
#[serde(default)]
pub organization: String,
}
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct BuildActionState {

View File

@@ -10,6 +10,7 @@ use strum::{Display, EnumString};
use typeshare::typeshare;
use super::{
build::ImageRegistry,
resource::{Resource, ResourceListItem, ResourceQuery},
EnvironmentVar, Version,
};
@@ -62,6 +63,17 @@ pub struct DeploymentConfig {
#[builder(default)]
pub image: DeploymentImage,
/// Configure the registry used to pull the image from the registry.
/// Used with `docker login`.
///
/// When using attached build as image source:
/// - If the field is `None` variant, will use the same ImageRegistry config as the build.
/// - Otherwise, it must match the variant of the ImageRegistry build config.
/// - Only the account is used, the organization is not needed here
#[serde(default)]
#[builder(default)]
pub image_registry: ImageRegistry,
/// Whether to skip secret interpolation into the deployment environment variables.
#[serde(default)]
#[builder(default)]
@@ -138,13 +150,6 @@ pub struct DeploymentConfig {
#[serde(default)]
#[builder(default)]
pub extra_args: Vec<String>,
/// The docker account the deployment should use to pull the image.
/// - If using a custom image, empty string means don't use an account. Only works for public images.
/// - If using a monitor build, empty string means to use the same docker account as the build uses.
#[serde(default)]
#[builder(default)]
pub docker_account: String,
}
impl DeploymentConfig {
@@ -175,6 +180,7 @@ impl Default for DeploymentConfig {
server_id: Default::default(),
send_alerts: default_send_alerts(),
image: Default::default(),
image_registry: Default::default(),
skip_secret_interp: Default::default(),
redeploy_on_build: Default::default(),
term_signal_labels: default_term_signal_labels(),
@@ -188,7 +194,6 @@ impl Default for DeploymentConfig {
restart: Default::default(),
command: Default::default(),
extra_args: Default::default(),
docker_account: Default::default(),
}
}
}

View File

@@ -2,6 +2,7 @@ use std::str::FromStr;
use anyhow::Context;
use async_timing_util::unix_timestamp_ms;
use build::CloudRegistryConfig;
use clap::Parser;
use derive_empty_traits::EmptyTraits;
use serde::{
@@ -105,22 +106,44 @@ pub fn optional_string(string: &str) -> Option<String> {
pub fn get_image_name(
build::Build {
name,
config:
build::BuildConfig {
docker_organization,
docker_account,
..
},
config: build::BuildConfig { image_registry, .. },
..
}: &build::Build,
) -> String {
let name = to_monitor_name(name);
if !docker_organization.is_empty() {
format!("{docker_organization}/{name}")
} else if !docker_account.is_empty() {
format!("{docker_account}/{name}")
} else {
name
match image_registry {
// TODO
build::ImageRegistry::None(_) => name,
build::ImageRegistry::DockerHub(CloudRegistryConfig {
organization,
account,
}) => {
if !organization.is_empty() {
format!("{organization}/{name}")
} else if !account.is_empty() {
format!("{account}/{name}")
} else {
name
}
}
build::ImageRegistry::GithubContainerRegistry(
CloudRegistryConfig {
organization,
account,
},
) => {
if !organization.is_empty() {
format!("ghcr.io/{organization}/{name}")
} else if !account.is_empty() {
format!("ghcr.io/{account}/{name}")
} else {
name
}
}
build::ImageRegistry::Custom(_) => {
// TODO
name
}
}
}

View File

@@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize};
#[response(BuildResponse)]
pub struct Build {
pub build: monitor_client::entities::build::Build,
/// Override docker token with one sent from core.
pub docker_token: Option<String>,
/// Override registry token with one sent from core.
pub registry_token: Option<String>,
/// Propogate any secret replacers from core interpolation.
pub replacers: Vec<(String, String)>,
}

View File

@@ -101,10 +101,10 @@ pub struct PruneContainers {}
#[response(Log)]
pub struct Deploy {
pub deployment: Deployment,
/// Override docker token with one sent from core.
pub docker_token: Option<String>,
pub stop_signal: Option<TerminationSignal>,
pub stop_time: Option<i32>,
/// Override registry token with one sent from core.
pub registry_token: Option<String>,
/// Propogate any secret replacers from core interpolation.
pub replacers: Vec<(String, String)>,
}