Commit 495658b4 authored by Wohlgemuth, Jason's avatar Wohlgemuth, Jason
Browse files

feat: Build out GitLab API support

parent 8df5a02e
Loading
Loading
Loading
Loading
Loading
+23 −7
Original line number Diff line number Diff line
use acorn::io::api;
use acorn::io::api::{self};
use acorn::param;
use acorn::prelude::PathBuf;
use clap_verbosity_flag::Verbosity;
use color_eyre::eyre::{Report, Result};

pub async fn run(_path: &Option<PathBuf>, _ignore: &Option<String>, _offline: &bool, _verbose: &Verbosity) -> Result<(), Report> {
    let countries = api::geonames::get_countries().await;
    println!("Countries: {:#?}", countries);
    println!("CiteAs API is healthy: {}", api::citeas::is_healthy().await);
    println!("ORCiD API is healthy: {}", api::orcid::is_healthy().await);
    println!("ROR API is healthy: {}", api::ror::is_healthy().await);
    println!("Licenses: {:#?}", api::spdx::Licenses::download().await);
    // let countries = api::geonames::get_countries().await;
    // println!("Countries: {:#?}", countries);
    // println!("CiteAs API is healthy: {}", api::citeas::is_healthy().await);
    // println!("ORCiD API is healthy: {}", api::orcid::is_healthy().await);
    // println!("ROR API is healthy: {}", api::ror::is_healthy().await);
    // println!("Licenses: {:#?}", api::spdx::Licenses::download().await);
    gitlab_example().await;
    // orcid_example().await;
    // ror_example().await;
    Ok(())
}
#[allow(dead_code)]
async fn gitlab_example() {
    let _ = dotenvy::from_filename(".env");
    match std::env::var("GITLAB_TOKEN") {
        | Ok(token) => {
            let project = "34619";
            let group = "24758";
            println!("GitLab Token: {}", token);
            println!("Runners: {:#?}", api::gitlab::runners(&token).await);
            println!("Runner: {:#?}", api::gitlab::runner(project, &token).await);
            println!("Groups: {:#?}", api::gitlab::groups(group, &token).await);
        }
        | Err(_) => println!("GitLab Token not found in environment variables"),
    }
}
#[allow(dead_code)]
async fn orcid_example() {
    let params = vec![
        param!(
+6 −6
Original line number Diff line number Diff line
@@ -8,7 +8,7 @@ use serde_with::skip_serializing_none;
/// [GitHub]: https://docs.github.com/en/rest
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GithubTreeEntry {
pub struct TreeEntry {
    /// Path of tree entry
    ///
    /// The path inside the repository. Used to get content of subdirectories.
@@ -42,7 +42,7 @@ pub struct GithubTreeEntry {
///   "truncated": false
/// }
/// ```
/// where `"tree"` is a list of [GithubTreeEntry].
/// where `"tree"` is a list of [TreeEntry].
///
/// ### Example Endpoint
/// > `https://api.github.com/repos/jhwohlgemuth/pwsh-prelude/git/trees/master?recursive=1`
@@ -52,17 +52,17 @@ pub struct GithubTreeEntry {
/// [GitHub]: https://docs.github.com/en/rest
/// [documentation]: https://docs.github.com/en/rest/git/trees?apiVersion=2022-11-28#get-a-tree
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GithubTreeResponse {
pub struct TreeResponse {
    /// SHA1 of tree
    pub sha: String,
    /// URL of associated data API endpoint
    pub url: String,
    /// List of [GithubTreeEntry]
    pub tree: Vec<GithubTreeEntry>,
    /// List of [TreeEntry]
    pub tree: Vec<TreeEntry>,
    /// Whether tree is truncated
    pub truncated: bool,
}
impl GithubTreeEntry {
impl TreeEntry {
    /// Get path of tree entry
    pub fn path(self) -> String {
        self.path
+354 −5
Original line number Diff line number Diff line
//! Module for interacting with GitLab API
//!
use crate::io::api::TreeEntryType;
// TODO: Add custom field list and query pair type
// TODO: Finish modeling necessary API endpoints
use crate::io::api::{RemoteResource, Searchable, TreeEntryType, INCLUDED_ENDPOINTS};
use crate::io::config::{RunnerStatus, RunnerType};
use crate::param;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

/// Type for GitLab API descendent groups response
pub type GroupsResponse = Vec<GroupDetails>;
/// Type for GitLab API all runners response
pub type RunnersResponse = Vec<RunnerDetails>;
/// Type for GitLab API response for a tree
pub type GitlabTreeResponse = Vec<GitlabTreeEntry>;
pub type TreeResponse = Vec<TreeEntry>;
/// Access level of the runner
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AccessLevel {
    /// Not protected
    NotProtected,
    /// Ref protected
    RefProtected,
}
/// Group visibility level
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GroupVisibility {
    /// Public visibility
    Public,
    /// Internal visibility
    Internal,
    /// Private visibility
    Private,
}
/// Runner group details
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GroupDetails {
    /// Numeric ID of the group
    #[serde(rename = "id")]
    pub identifier: u64,
    /// URL of the group page
    #[serde(rename = "web_url")]
    pub url: String,
    /// Group name
    pub name: String,
    /// Group path
    pub path: Option<String>,
    /// Group description
    pub description: Option<String>,
    /// Whether emails are disabled
    #[serde(default)]
    pub emails_disabled: bool,
    /// Whether emails are enabled
    #[serde(default)]
    pub emails_enabled: bool,
    /// Whether diff previews appear in emails
    #[serde(default)]
    pub show_diff_preview_in_email: bool,
    /// Group visibility level
    pub visibility: Option<GroupVisibility>,
    /// Whether sharing with other groups is locked
    #[serde(default)]
    pub share_with_group_lock: bool,
    /// Whether two-factor authentication is required
    #[serde(default)]
    pub require_two_factor_authentication: bool,
    /// Whether LFS is enabled
    #[serde(default)]
    pub lfs_enabled: bool,
    /// Whether the group is archived
    #[serde(default)]
    pub archived: bool,
    /// Duo features enabled flag
    #[serde(default)]
    pub duo_features_enabled: bool,
    /// Duo features lock flag
    #[serde(default)]
    pub lock_duo_features_enabled: bool,
    /// Auto Duo code review enabled flag
    #[serde(default)]
    pub auto_duo_code_review_enabled: bool,
    /// Whether math rendering limits are enabled
    #[serde(default)]
    pub math_rendering_limits_enabled: bool,
    /// Whether math rendering limits are locked
    #[serde(default)]
    pub lock_math_rendering_limits_enabled: bool,
    /// Whether access requests are enabled
    #[serde(default)]
    pub request_access_enabled: bool,
    /// Grace period for two-factor authentication
    pub two_factor_grace_period: Option<u64>,
    /// Project creation level
    pub project_creation_level: Option<String>,
    /// Auto DevOps enabled flag
    pub auto_devops_enabled: Option<bool>,
    /// Subgroup creation level
    pub subgroup_creation_level: Option<String>,
    /// Whether mentions are disabled
    pub mentions_disabled: Option<bool>,
    /// Default branch name
    pub default_branch: Option<String>,
    /// Default branch protection mode
    pub default_branch_protection: Option<u64>,
    /// Default branch protection policy details
    pub default_branch_protection_defaults: Option<RunnerGroupBranchProtectionDefaults>,
    /// Group avatar URL
    #[serde(rename = "avatar_url")]
    pub avatar_url: Option<String>,
    /// Group full display name
    pub full_name: Option<String>,
    /// Group full path
    pub full_path: Option<String>,
    /// Group creation timestamp in ISO-8601 format
    pub created_at: Option<String>,
    /// Parent group identifier
    pub parent_id: Option<u64>,
    /// Organization identifier
    pub organization_id: Option<u64>,
    /// Shared runners setting
    pub shared_runners_setting: Option<String>,
    /// Maximum artifacts size limit
    pub max_artifacts_size: Option<u64>,
    /// Group deletion schedule date
    pub marked_for_deletion_on: Option<String>,
    /// LDAP common name
    pub ldap_cn: Option<String>,
    /// LDAP access value
    pub ldap_access: Option<String>,
    /// File template project identifier
    pub file_template_project_id: Option<u64>,
    /// Wiki access level
    pub wiki_access_level: Option<String>,
    /// Duo core features enabled flag
    pub duo_core_features_enabled: Option<bool>,
}
/// GitLab API response for runner details
/// ### Example JSON response
/// ```json
/// {
///     "active": true,
///     "paused": false,
///     "architecture": null,
///     "description": "test-1-20150125",
///     "id": 6,
///     "ip_address": "",
///     "is_shared": false,
///     "runner_type": "project_type",
///     "contacted_at": "2016-01-25T16:39:48.066Z",
///     "maintenance_note": null,
///     "name": null,
///     "online": true,
///     "status": "online",
///     "platform": null,
///     "projects": [
///         {
///             "id": 1,
///             "name": "GitLab Community Edition",
///             "name_with_namespace": "GitLab.org / GitLab Community Edition",
///             "path": "gitlab-foss",
///             "path_with_namespace": "gitlab-org/gitlab-foss"
///         }
///     ],
///     "revision": null,
///     "tag_list": [
///         "ruby",
///         "mysql"
///     ],
///     "version": null,
///     "access_level": "ref_protected",
///     "maximum_timeout": 3600
/// }
/// ```
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunnerDetails {
    /// Numeric ID of the runner
    #[serde(rename = "id")]
    pub identifier: u64,
    /// Whether the runner is active
    #[serde(default)]
    pub active: bool,
    /// Whether the runner is online
    #[serde(default)]
    pub online: bool,
    /// Whether the runner is paused
    #[serde(default)]
    pub paused: bool,
    /// Whether the runner is shared
    #[serde(default)]
    #[serde(rename = "is_shared")]
    pub shared: bool,
    /// CPU architecture reported by the runner
    pub architecture: Option<String>,
    /// Runner description
    pub description: Option<String>,
    /// Runner IP address
    pub ip_address: Option<String>,
    /// Type of runner (for example, `project_type`)
    pub runner_type: Option<RunnerType>,
    /// Created by user
    pub created_by: Option<UserDetails>,
    /// Created timestamp in ISO-8601 format
    pub created_at: Option<String>,
    /// Last contact timestamp in ISO-8601 format
    pub contacted_at: Option<String>,
    /// Optional maintenance note
    pub maintenance_note: Option<String>,
    /// Optional display name
    pub name: Option<String>,
    /// Current runner status
    pub status: Option<RunnerStatus>,
    /// Current job execution status
    pub job_execution_status: Option<String>,
    /// Optional platform string
    pub platform: Option<String>,
    /// Projects associated with this runner
    pub projects: Option<Vec<RunnerScope>>,
    /// Groups associated with this runner
    pub groups: Option<Vec<RunnerScope>>,
    /// Optional Git revision for the runner version
    pub revision: Option<String>,
    /// Runner tags
    #[serde(rename = "tag_list")]
    pub tags: Option<Vec<String>>,
    /// Optional runner version
    pub version: Option<String>,
    /// Access level for this runner
    pub access_level: Option<AccessLevel>,
    /// Maximum timeout in seconds
    pub maximum_timeout: Option<u64>,
}
/// Access level entry for branch protection settings
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunnerGroupAccessLevel {
    /// Numeric access level
    pub access_level: u64,
}
/// Runner group branch protection defaults
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunnerGroupBranchProtectionDefaults {
    /// Access levels allowed to push
    pub allowed_to_push: Vec<RunnerGroupAccessLevel>,
    /// Whether force push is allowed
    pub allow_force_push: bool,
    /// Access levels allowed to merge
    pub allowed_to_merge: Vec<RunnerGroupAccessLevel>,
}
/// Runner scope details for project or group entries
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunnerScope {
    /// Numeric ID of the scope entry
    #[serde(rename = "id")]
    pub identifier: u64,
    /// Scope name
    pub name: String,
    /// Project path
    pub path: Option<String>,
    /// Project name including namespace
    pub name_with_namespace: Option<String>,
    /// Project path including namespace
    pub path_with_namespace: Option<String>,
    /// URL of the group page
    #[serde(rename = "web_url")]
    pub url: Option<String>,
}
/// Struct for GitLab tree entry
///
/// See <https://docs.gitlab.com/api/repositories/#list-repository-tree>
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GitlabTreeEntry {
pub struct TreeEntry {
    /// Integer ID of GitLab project
    ///
    /// See <https://docs.gitlab.com/api/projects/#get-a-single-project> for more information
@@ -23,12 +287,49 @@ pub struct GitlabTreeEntry {
    pub entry_type: TreeEntryType,
    /// Path of tree entry
    ///
    /// The path inside the repository. Used to get content of subdirectories.
    /// The path inside the repository Used to get content of subdirectories
    pub path: String,
    /// Mode of tree entry
    pub mode: String,
}
impl GitlabTreeEntry {
/// User details
/// ### Example JSON response
/// ```json
/// {
///     "avatar_url": String("https://code.ornl.gov/uploads/-/system/user/avatar/4862/avatar.png"),
///     "id": Number(4862),
///     "locked": Bool(false),
///     "name": String("Wohlgemuth, Jason"),
///     "public_email": String("wohlgemuthjh@ornl.gov"),
///     "state": String("active"),
///     "username": String("o9w"),
///     "web_url": String("https://code.ornl.gov/o9w"),
/// }
/// ```
#[skip_serializing_none]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserDetails {
    /// URL of user avatar image
    pub avatar_url: String,
    /// Numeric ID of user
    #[serde(rename = "id")]
    pub identifier: u64,
    /// Whether the user is locked
    pub locked: bool,
    /// User's full name
    pub name: String,
    /// User's public email address
    #[serde(rename = "public_email")]
    pub email: Option<String>,
    /// User state (for example, "active")
    pub state: String,
    /// Username/handle of the user
    pub username: String,
    /// URL of the user's profile page
    #[serde(rename = "web_url")]
    pub url: String,
}
impl TreeEntry {
    /// Get path of tree entry
    pub fn path(self) -> String {
        self.path
@@ -39,3 +340,51 @@ impl GitlabTreeEntry {
        entry_type.eq(&TreeEntryType::Blob)
    }
}
/// Get descendant groups of a group by identifier
pub async fn groups(identifier: impl Into<String>, token: impl Into<String>) -> Result<GroupsResponse, String> {
    let name = "GitLab";
    let action = "groups";
    match INCLUDED_ENDPOINTS.find_by_name(name) {
        | Some(endpoint) => {
            let params = vec![
                param!(Header, "PRIVATE-TOKEN", &token.into()),
                param!(TemplateValue, "identifier", &identifier.into()),
                // param!(FieldList, "page", "1"),
                param!(FieldList, "per_page", "100"),
            ];
            let response = endpoint.invoke(action, Some(params)).await;
            endpoint.handle::<GroupsResponse>(response)
        }
        | None => Err(format!("{name} API endpoint not found")),
    }
}
/// Get runner details by identifier
pub async fn runner(identifier: impl Into<String>, token: impl Into<String>) -> Result<RunnerDetails, String> {
    let name = "GitLab";
    let action = "runners";
    match INCLUDED_ENDPOINTS.find_by_name(name) {
        | Some(endpoint) => {
            let params = vec![
                param!(Header, "PRIVATE-TOKEN", &token.into()),
                param!(TemplateValue, "identifier", &identifier.into()),
            ];
            let response = endpoint.invoke(action, Some(params)).await;
            dbg!(&response);
            endpoint.handle::<RunnerDetails>(response)
        }
        | None => Err(format!("{name} API endpoint not found")),
    }
}
/// Get runners visible to user associated with given token
pub async fn runners(token: impl Into<String>) -> Result<RunnersResponse, String> {
    let name = "GitLab";
    let action = "runners";
    match INCLUDED_ENDPOINTS.find_by_name(name) {
        | Some(endpoint) => {
            let params = vec![param!(Header, "PRIVATE-TOKEN", &token.into())];
            let response = endpoint.invoke(action, Some(params)).await;
            endpoint.handle::<RunnersResponse>(response)
        }
        | None => Err(format!("{name} API endpoint not found")),
    }
}
+24 −3
Original line number Diff line number Diff line
@@ -4,8 +4,8 @@
//!
//! The ACORN configuration file configures what buckets should be downloaded, readability analysis, <span title="Large Language Model">LLM</span> settings, and more.
//!
use crate::io::api::github::{GithubTreeEntry, GithubTreeResponse};
use crate::io::api::gitlab::{GitlabTreeEntry, GitlabTreeResponse};
use crate::io::api::github::{TreeEntry as GithubTreeEntry, TreeResponse as GithubTreeResponse};
use crate::io::api::gitlab::{TreeEntry as GitlabTreeEntry, TreeResponse as GitlabTreeResponse};
use crate::io::api::Endpoint;
use crate::io::{files_all, get, parent, read_file, write_file, FromPath, InputOutput};
use crate::prelude::{self, create_dir_all, exit, io, Cursor, Error, File, PathBuf};
@@ -25,22 +25,43 @@ use serde_with::skip_serializing_none;
use tracing::{debug, error, trace, warn};

const IGNORE: [&str; 5] = [".gitignore", ".gitlab-ci.yml", ".gitkeep", ".DS_Store", "README.md"];

/// Runner status for CI/CD pipelines
#[derive(Clone, Debug, Default, Display, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunnerStatus {
    /// Online and available to run jobs
    #[default]
    Online,
    /// Offline and/or unavailable to run jobs
    Offline,
    /// Runner has not contacted the server for a while
    Stale,
    /// Runner has never contacted the server
    NeverContacted,
    /// Deprecated
    Active,
    /// Deprecated
    Paused,
}
/// Runner types for CI/CD pipelines
///
/// Mostly for GitLab as GitHub only has two types: hosted (by GitHub) and self-hosted
#[derive(Clone, Debug, Default, Display, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RunnerType {
    /// Accessible to a specific group and its projects/subgroups
    #[default]
    #[display("group_type")]
    #[serde(rename = "group_type", alias = "group")]
    Group,
    /// Available to all projects and groups within an instance
    /// > Also called "shared runners" in GitLab
    #[display("instance_type")]
    #[serde(rename = "instance_type", alias = "instance")]
    Instance,
    /// Available to a specific project
    #[display("project_type")]
    #[serde(rename = "project_type", alias = "project")]
    Project,
}
/// Struct for application configuration
+2 −2
Original line number Diff line number Diff line
use crate::io::api::gitlab::GitlabTreeEntry;
use crate::io::api::gitlab::TreeEntry;
use crate::io::api::TreeEntryType;
use crate::io::bagit::{Bag, BagInfo, Save};
use crate::io::config::{ApplicationConfiguration, Bucket};
@@ -132,7 +132,7 @@ fn test_bucket() {
}
#[test]
fn test_gitlab_tree_entry() {
    let entry = GitlabTreeEntry {
    let entry = TreeEntry {
        id: 1234.to_string(),
        name: "acorn".to_string(),
        entry_type: TreeEntryType::Tree,
Loading