Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cache): add support for specifying keypair path in cache creation and configuration #221

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions client/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,24 @@ impl ApiClient {
}
}

/// Deletes a store path from a cache.
pub async fn delete_path(&self, cache: &CacheName, store_path_hash: &StorePathHash) -> Result<()> {
let endpoint = self
.endpoint
.join("_api/v1/delete-path")?
.join(cache.as_str())?
.join(store_path_hash.as_str())?;

let res = self.client.delete(endpoint).send().await?;

if res.status().is_success() {
Ok(())
} else {
let api_error = ApiError::try_from_response(res).await?;
Err(api_error.into())
}
}

/// Destroys a cache.
pub async fn destroy_cache(&self, cache: &CacheName) -> Result<()> {
let endpoint = self
Expand Down
69 changes: 67 additions & 2 deletions client/src/command/cache.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use attic::nix_store::{NixStore, StorePath};
use clap::{Parser, Subcommand};
use dialoguer::Input;
use humantime::Duration;
Expand All @@ -10,6 +11,7 @@ use crate::config::Config;
use attic::api::v1::cache_config::{
CacheConfig, CreateCacheRequest, KeypairConfig, RetentionPeriodConfig,
};
use attic::signing::NixKeypair;

/// Manage caches on an Attic server.
#[derive(Debug, Parser)]
Expand All @@ -22,6 +24,7 @@ pub struct Cache {
enum Command {
Create(Create),
Configure(Configure),
DeletePath(DeletePath),
Destroy(Destroy),
Info(Info),
}
Expand Down Expand Up @@ -72,6 +75,12 @@ struct Create {
default_value = "cache.nixos.org-1"
)]
upstream_cache_key_names: Vec<String>,

/// The signing keypair to use for the cache.
///
/// If not specified, a new keypair will be generated.
#[clap(long)]
keypair_path: Option<String>,
}

/// Configure a cache.
Expand All @@ -91,6 +100,14 @@ struct Configure {
#[clap(long)]
regenerate_keypair: bool,

/// Set a keypair for the cache.
///
/// The server-side signing key will be set to the
/// specified keypair. This is useful for setting up
/// a cache with a pre-existing keypair.
#[clap(long, conflicts_with = "regenerate_keypair")]
keypair_path: Option<String>,

/// Make the cache public.
///
/// Use `--private` to make it private.
Expand Down Expand Up @@ -137,6 +154,23 @@ struct Configure {
reset_retention_period: bool,
}

/// Delete a path from a cache.
///
/// This command is used to delete a path from a cache.
///
/// You need the `delete` permission on the cache that
/// you are deleting from.
///
/// The path is specified as a store path.
#[derive(Debug, Clone, Parser)]
struct DeletePath {
/// Name of the cache to delete from.
cache: CacheRef,

/// The store path to delete.
store_path: PathBuf,
}

/// Destroy a cache.
///
/// Destroying a cache causes it to become unavailable but the
Expand Down Expand Up @@ -168,6 +202,7 @@ pub async fn run(opts: Opts) -> Result<()> {
match &sub.command {
Command::Create(sub) => create_cache(sub.to_owned()).await,
Command::Configure(sub) => configure_cache(sub.to_owned()).await,
Command::DeletePath(sub) => delete_path(sub.to_owned()).await,
Command::Destroy(sub) => destroy_cache(sub.to_owned()).await,
Command::Info(sub) => show_cache_config(sub.to_owned()).await,
}
Expand All @@ -179,9 +214,14 @@ async fn create_cache(sub: Create) -> Result<()> {
let (server_name, server, cache) = config.resolve_cache(&sub.cache)?;
let api = ApiClient::from_server_config(server.clone())?;

let mut keypair = KeypairConfig::Generate;
if let Some(keypair_path) = &sub.keypair_path {
let contents = std::fs::read_to_string(keypair_path)?;
keypair = KeypairConfig::Keypair(NixKeypair::from_str(&contents)?);
}

let request = CreateCacheRequest {
// TODO: Make this configurable?
keypair: KeypairConfig::Generate,
keypair,
is_public: sub.public,
priority: sub.priority,
store_dir: sub.store_dir,
Expand Down Expand Up @@ -230,6 +270,10 @@ async fn configure_cache(sub: Configure) -> Result<()> {

if sub.regenerate_keypair {
patch.keypair = Some(KeypairConfig::Generate);
} else if let Some(keypair_path) = &sub.keypair_path {
let contents = std::fs::read_to_string(keypair_path)?;
let keypair = KeypairConfig::Keypair(NixKeypair::from_str(&contents)?);
patch.keypair = Some(keypair);
}

patch.store_dir = sub.store_dir;
Expand All @@ -248,6 +292,27 @@ async fn configure_cache(sub: Configure) -> Result<()> {
Ok(())
}

async fn delete_path(sub: DeletePath) -> Result<()> {
let config = Config::load()?;

let (server_name, server, cache) = config.resolve_cache(&sub.cache)?;
let api = ApiClient::from_server_config(server.clone())?;

let store = NixStore::connect()?;

api.delete_path(cache, &store.parse_store_path(&sub.store_path)?.to_hash())
.await?;

eprintln!(
"🗑️ Deleted path \"{}\" from cache \"{}\" on \"{}\"",
sub.store_path,
cache.as_str(),
server_name.as_str()
);

Ok(())
}

async fn destroy_cache(sub: Destroy) -> Result<()> {
let config = Config::load()?;

Expand Down
35 changes: 35 additions & 0 deletions integration-tests/basic/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,41 @@ in {
client.succeed(f"nix-store --delete {test2_file}")
client.succeed(f"nix-store -r {test2_file}")

with subtest("Check that we can set the keypair using an already exisiting one"):
client.succeed("nix-store --generate-binary-cache-key cache.example.com-1 ./cache-key.secret ./cache-key.pub")
pubkey = client.succeed("cat ./cache-key.pub").strip()
client.succeed("attic cache configure test --keypair-path ./cache-key.secret")
client.succeed("attic use root:test")
client.succeed(f"nix-store -r {test_file}")
cache_info = client.succeed("attic cache info test 2>&1")
assert pubkey in cache_info, f"LHS: {pubkey}, RHS: {cache_info}"

with subtest("Check that we can delete a path"):
# first build a new path
client.succeed("${makeTestDerivation} test3.nix")
# then push it
test3_file = client.succeed("nix-build --no-out-link test3.nix")
client.succeed(f"attic push test {test3_file}")
# then delete it locally
client.succeed(f"nix-store --delete {test3_file}")
# then pull it back
client.succeed(f"nix-store -r {test3_file}")
# then delete it from the cache and from the local store
client.succeed(f"attic cache delete test {test3_file}")
client.succeed(f"nix-store --delete {test3_file}")
# then check that it's not there anymore
client.fail(f"nix-store -r {test3_file}")

with subtest("Check that we can create a new store using an already exisiting keypair"):
client.succeed("attic cache create test3 --keypair-path ./cache-key.secret")
client.succeed("attic use root:test3")
test3_file = client.succeed("nix-build --no-out-link test.nix")
client.succeed(f"attic push test3 {test3_file}")
client.succeed(f"nix-store --delete {test3_file}")
client.succeed(f"nix-store -r {test3_file}")
cache_info = client.succeed("attic cache info test3 2>&1")
assert pubkey in cache_info, f"LHS: {pubkey}, RHS: {cache_info}"

with subtest("Check that we can destroy the cache"):
client.succeed("attic cache info test")
client.succeed("attic cache destroy --no-confirm test")
Expand Down
46 changes: 46 additions & 0 deletions server/src/api/v1/delete_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::collections::HashSet;

use axum::extract::{Extension, Json, Path};
use sea_orm::entity::prelude::*;
use sea_orm::{FromQueryResult, QuerySelect};
use tracing::instrument;

use crate::database::entity::cache;
use crate::database::entity::nar;
use crate::database::entity::object::{self, Entity as Object};
use crate::error::{ServerError, ServerResult};
use crate::{RequestState, State};
use attic::api::v1::get_missing_paths::{GetMissingPathsRequest, GetMissingPathsResponse};
use attic::nix_store::StorePathHash;


/// Deletes a path from a cache.
#[instrument(skip_all, fields(cache_name, path))]
pub(crate) async fn delete_path(
Extension(state): Extension<State>,
Extension(req_state): Extension<RequestState>,
Path((cache_name, path)): Path<(CacheName, String)>,
) -> ServerResult<()> {
let database = state.database().await?;
let cache = req_state
.auth
.auth_cache(database, &cache_name, |cache, permission| {
permission.require_delete()?;
Ok(cache)
})
.await?;

let object = Object::find()
.filter(object::Column::StorePathHash.eq(store_path_hash.as_str()))
.one(database)
.await
.map_err(ServerError::database_error)?;

if let Some(object) = object {
object.delete(database).await.map_err(ServerError::database_error)?;
} else {
return Err(ServerError::not_found("object not found"));
}

Ok(())
}
5 changes: 5 additions & 0 deletions server/src/api/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod cache_config;
mod get_missing_paths;
mod upload_path;
mod delete_path;

use axum::{
routing::{delete, get, patch, post, put},
Expand Down Expand Up @@ -30,6 +31,10 @@ pub(crate) fn get_router() -> Router {
"/_api/v1/cache-config/:cache",
patch(cache_config::configure_cache),
)
.route(
"/_api/v1/delete-path/:cache/:store_path_hash",
delete(delete_path::delete_path),
)
.route(
"/_api/v1/cache-config/:cache",
delete(cache_config::destroy_cache),
Expand Down