diff --git a/example_pipeline_run.json b/example_pipeline_run.json deleted file mode 100644 index 347f613..0000000 --- a/example_pipeline_run.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "apiVersion": "tekton.dev/v1", - "kind": "PipelineRun", - "metadata": { - "name": "minimal-test", - "namespace": "lighthouse" - }, - "spec": { - "pipelineSpec": { - "tasks": [ - { - "name": "echo-variable", - "taskSpec": { - "metadata": {}, - "stepTemplate": { - "image": "alpine:3.18", - "computeResources": { - "requests": { - "cpu": "10m", - "memory": "600Mi" - } - } - }, - "steps": [ - { - "image": "alpine:3.18", - "script": "#!/usr/bin/env sh\necho \"The variable: $(params.LOREM)\"\n" - } - ] - }, - "params": [ - { - "name": "LOREM", - "value": "$(tasks.set-variable.results.ipsum)" - } - ] - }, - { - "name": "set-variable", - "taskSpec": { - "metadata": {}, - "stepTemplate": { - "image": "alpine:3.18", - "computeResources": { - "requests": { - "cpu": "10m", - "memory": "600Mi" - } - } - }, - "results": [ - { - "name": "ipsum" - } - ], - "steps": [ - { - "image": "alpine:3.18", - "script": "#!/usr/bin/env sh\necho -n \"dolar\" > \"$(results.ipsum.path)\"\n" - } - ] - } - } - ] - }, - "timeouts": { - "pipeline": "240h0m0s" - } - } -} diff --git a/example_pipeline_run.yaml b/example_pipeline_run.yaml deleted file mode 100644 index b663ce6..0000000 --- a/example_pipeline_run.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: tekton.dev/v1 -kind: PipelineRun -metadata: - name: minimal-test - namespace: lighthouse -spec: - pipelineSpec: - tasks: - - name: echo-variable - taskSpec: - metadata: {} - stepTemplate: - image: alpine:3.18 - computeResources: - requests: - cpu: 10m - memory: 600Mi - steps: - - image: alpine:3.18 - script: | - #!/usr/bin/env sh - echo "The variable: $(params.LOREM)" - params: - - name: LOREM - value: $(tasks.set-variable.results.ipsum) - - name: set-variable - taskSpec: - metadata: {} - stepTemplate: - image: alpine:3.18 - computeResources: - requests: - cpu: 10m - memory: 600Mi - results: - - name: ipsum - steps: - - image: alpine:3.18 - script: | - #!/usr/bin/env sh - echo -n "dolar" > "$(results.ipsum.path)" - timeouts: - pipeline: 240h0m0s diff --git a/example_webhook_payload.json b/example_webhook_payload.json new file mode 100644 index 0000000..4079cd1 --- /dev/null +++ b/example_webhook_payload.json @@ -0,0 +1,190 @@ +{ + "ref": "refs/heads/main", + "before": "d5902e3e7f62cbd86e095437ccc33e945fcb0791", + "after": "b8444344c4821e87a894cd195f8bec39cd501f68", + "compare_url": "https://code.fizz.buzz/talexander/webhook_bridge/compare/d5902e3e7f62cbd86e095437ccc33e945fcb0791...b8444344c4821e87a894cd195f8bec39cd501f68", + "commits": [ + { + "id": "b8444344c4821e87a894cd195f8bec39cd501f68", + "message": "Update PipelineRun to tekton v1 from v1beta.\n", + "url": "https://code.fizz.buzz/talexander/webhook_bridge/commit/b8444344c4821e87a894cd195f8bec39cd501f68", + "author": { + "name": "Tom Alexander", + "email": "tom@fizz.buzz", + "username": "" + }, + "committer": { + "name": "Tom Alexander", + "email": "tom@fizz.buzz", + "username": "" + }, + "verification": null, + "timestamp": "2024-09-28T20:33:35-04:00", + "added": [], + "removed": [], + "modified": [ + ".webhook_bridge/pipeline-format.yaml", + ".webhook_bridge/pipeline-rust-clippy.yaml", + ".webhook_bridge/pipeline-rust-test.yaml" + ] + } + ], + "total_commits": 1, + "head_commit": { + "id": "b8444344c4821e87a894cd195f8bec39cd501f68", + "message": "Update PipelineRun to tekton v1 from v1beta.\n", + "url": "https://code.fizz.buzz/talexander/webhook_bridge/commit/b8444344c4821e87a894cd195f8bec39cd501f68", + "author": { + "name": "Tom Alexander", + "email": "tom@fizz.buzz", + "username": "" + }, + "committer": { + "name": "Tom Alexander", + "email": "tom@fizz.buzz", + "username": "" + }, + "verification": null, + "timestamp": "2024-09-28T20:33:35-04:00", + "added": [], + "removed": [], + "modified": [ + ".webhook_bridge/pipeline-format.yaml", + ".webhook_bridge/pipeline-rust-clippy.yaml", + ".webhook_bridge/pipeline-rust-test.yaml" + ] + }, + "repository": { + "id": 21, + "owner": { + "id": 1, + "login": "talexander", + "login_name": "", + "full_name": "", + "email": "gitea@local.domain", + "avatar_url": "https://code.fizz.buzz/avatars/9d402a89b5a0786f83c1b8c5486fc7ff3d083a54fe20e55c0a776a1932c30289", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2023-07-05T22:03:28Z", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "talexander" + }, + "name": "webhook_bridge", + "full_name": "talexander/webhook_bridge", + "description": "A server that receives webhooks from gitea and fires off Tekton jobs in response.", + "empty": false, + "private": false, + "fork": false, + "template": false, + "parent": null, + "mirror": false, + "size": 286, + "language": "", + "languages_url": "https://code.fizz.buzz/api/v1/repos/talexander/webhook_bridge/languages", + "html_url": "https://code.fizz.buzz/talexander/webhook_bridge", + "url": "https://code.fizz.buzz/api/v1/repos/talexander/webhook_bridge", + "link": "", + "ssh_url": "git@code.fizz.buzz:talexander/webhook_bridge.git", + "clone_url": "https://code.fizz.buzz/talexander/webhook_bridge.git", + "original_url": "", + "website": "", + "stars_count": 0, + "forks_count": 0, + "watchers_count": 1, + "open_issues_count": 0, + "open_pr_counter": 0, + "release_counter": 0, + "default_branch": "main", + "archived": false, + "created_at": "2024-07-14T18:48:52Z", + "updated_at": "2024-09-28T23:43:53Z", + "archived_at": "1970-01-01T00:00:00Z", + "permissions": { + "admin": true, + "push": true, + "pull": true + }, + "has_issues": true, + "internal_tracker": { + "enable_time_tracker": true, + "allow_only_contributors_to_track_time": true, + "enable_issue_dependencies": true + }, + "has_wiki": true, + "has_pull_requests": true, + "has_projects": true, + "has_releases": true, + "has_packages": true, + "has_actions": false, + "ignore_whitespace_conflicts": false, + "allow_merge_commits": true, + "allow_rebase": true, + "allow_rebase_explicit": true, + "allow_squash_merge": true, + "allow_rebase_update": true, + "default_delete_branch_after_merge": false, + "default_merge_style": "merge", + "default_allow_maintainer_edit": false, + "avatar_url": "", + "internal": false, + "mirror_interval": "", + "mirror_updated": "0001-01-01T00:00:00Z", + "repo_transfer": null + }, + "pusher": { + "id": 1, + "login": "talexander", + "login_name": "", + "full_name": "", + "email": "talexander@noreply.code.fizz.buzz", + "avatar_url": "https://code.fizz.buzz/avatars/9d402a89b5a0786f83c1b8c5486fc7ff3d083a54fe20e55c0a776a1932c30289", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2023-07-05T22:03:28Z", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "talexander" + }, + "sender": { + "id": 1, + "login": "talexander", + "login_name": "", + "full_name": "", + "email": "talexander@noreply.code.fizz.buzz", + "avatar_url": "https://code.fizz.buzz/avatars/9d402a89b5a0786f83c1b8c5486fc7ff3d083a54fe20e55c0a776a1932c30289", + "language": "", + "is_admin": false, + "last_login": "0001-01-01T00:00:00Z", + "created": "2023-07-05T22:03:28Z", + "restricted": false, + "active": false, + "prohibit_login": false, + "location": "", + "website": "", + "description": "", + "visibility": "public", + "followers_count": 0, + "following_count": 0, + "starred_repos_count": 0, + "username": "talexander" + } +} diff --git a/src/crd_pipeline_run.rs b/src/crd_pipeline_run.rs index d9e9eb9..8bc411d 100644 --- a/src/crd_pipeline_run.rs +++ b/src/crd_pipeline_run.rs @@ -14,11 +14,32 @@ use serde_json::Value; plural = "pipelineruns" )] #[kube(namespaced)] -pub struct PipelineRunSpec { +#[serde(deny_unknown_fields)] +pub(crate) struct PipelineRunSpec { /// Contents of the Pipeline #[serde(default, skip_serializing_if = "Option::is_none")] - pub pipelineSpec: Option, + pub(crate) pipelineSpec: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub timeouts: Option, + pub(crate) timeouts: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) taskRunTemplate: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) workspaces: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) params: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub(crate) struct PipelineParam { + /// Contents of the Pipeline + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) name: Option, + + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) value: Option, } diff --git a/src/gitea_client/mod.rs b/src/gitea_client/mod.rs index 8da158c..9de8df6 100644 --- a/src/gitea_client/mod.rs +++ b/src/gitea_client/mod.rs @@ -97,6 +97,7 @@ impl GiteaClient { /// A single API response for GetTree containing only one page. #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ResponseGetTree { sha: String, url: String, @@ -107,6 +108,7 @@ struct ResponseGetTree { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ResponseObjectReference { path: String, mode: String, @@ -144,6 +146,7 @@ impl TreeFileReference { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] struct ResponseReadFile { content: String, encoding: String, diff --git a/src/hook_push.rs b/src/hook_push.rs index fb8661e..64967e1 100644 --- a/src/hook_push.rs +++ b/src/hook_push.rs @@ -1,12 +1,17 @@ +use std::borrow::Cow; + +use regex::Regex; use serde::Deserialize; use serde_json::Value; #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookPush { #[serde(rename = "ref")] ref_field: String, before: String, + after: String, compare_url: String, commits: Vec, total_commits: u64, @@ -18,6 +23,7 @@ pub(crate) struct HookPush { #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookUser { id: u64, login: String, @@ -44,6 +50,7 @@ pub(crate) struct HookUser { #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookRepository { id: u64, owner: HookUser, @@ -104,6 +111,7 @@ pub(crate) struct HookRepository { #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookRepositoryPermissions { admin: bool, push: bool, @@ -112,6 +120,7 @@ pub(crate) struct HookRepositoryPermissions { #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookRepositoryInternalTracker { enable_time_tracker: bool, allow_only_contributors_to_track_time: bool, @@ -120,6 +129,7 @@ pub(crate) struct HookRepositoryInternalTracker { #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookCommit { id: String, message: String, @@ -135,8 +145,48 @@ pub(crate) struct HookCommit { #[allow(dead_code)] #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct HookGitUser { name: String, email: String, username: String, } + +pub(crate) trait PipelineParamters { + fn get_pull_base_ref(&self) -> Result, Box>; + + fn get_pull_base_sha(&self) -> Result, Box>; + + fn get_repo_url(&self) -> Result, Box>; + + fn get_repo_name(&self) -> Result, Box>; + + fn get_repo_owner(&self) -> Result, Box>; +} + +impl PipelineParamters for HookPush { + fn get_pull_base_ref(&self) -> Result, Box> { + let ref_to_branch_regex = Regex::new(r"refs/heads/(?P.+)")?; + let captures = ref_to_branch_regex + .captures(self.ref_field.as_str()) + .ok_or("Could not find branch name.")?; + let branch = &captures["branch"]; + Ok(Cow::Owned(branch.to_owned())) + } + + fn get_pull_base_sha(&self) -> Result, Box> { + Ok(Cow::Borrowed(self.after.as_str())) + } + + fn get_repo_url(&self) -> Result, Box> { + Ok(Cow::Borrowed(self.repository.clone_url.as_str())) + } + + fn get_repo_name(&self) -> Result, Box> { + Ok(Cow::Borrowed(self.repository.name.as_str())) + } + + fn get_repo_owner(&self) -> Result, Box> { + Ok(Cow::Borrowed(self.repository.owner.username.as_str())) + } +} diff --git a/src/in_repo_config.rs b/src/in_repo_config.rs index 478741d..0643e35 100644 --- a/src/in_repo_config.rs +++ b/src/in_repo_config.rs @@ -4,6 +4,7 @@ use serde::Serialize; /// The webhook_bridge.toml file that lives inside repos that have their CI triggered by webhook_bridge. #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub(crate) struct InRepoConfig { pub(crate) version: String, @@ -13,6 +14,7 @@ pub(crate) struct InRepoConfig { /// A config for a job that is triggered by a push to a git repo. #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(deny_unknown_fields)] pub(crate) struct TriggerPush { pub(crate) name: String, pub(crate) source: String, diff --git a/src/kubernetes.rs b/src/kubernetes.rs new file mode 100644 index 0000000..3de7b6f --- /dev/null +++ b/src/kubernetes.rs @@ -0,0 +1,69 @@ +use kube::api::PostParams; +use kube::Api; +use kube::CustomResourceExt; +use tracing::debug; + +use crate::crd_pipeline_run::PipelineParam; +use crate::crd_pipeline_run::PipelineRun; +use crate::discovery::PipelineTemplate; +use crate::hook_push::HookPush; +use crate::hook_push::PipelineParamters; + +pub(crate) async fn run_pipelines( + hook: HookPush, + pipelines: Vec, + kubernetes_client: kube::Client, +) -> Result<(), Box> { + let jobs: Api = Api::namespaced(kubernetes_client, "lighthouse"); + // let jobs: Api = Api::default_namespaced(kubernetes_client); + tracing::info!("Using crd: {}", serde_json::to_string(&PipelineRun::crd())?); + + for mut pipeline in pipelines { + debug!("Kicking off {}", pipeline.name); + if pipeline.pipeline.spec.params.is_none() { + pipeline.pipeline.spec.params = Some(Vec::new()); + } + if let Some(param_list) = pipeline.pipeline.spec.params.as_mut() { + param_list.push(PipelineParam { + name: Some("JOB_NAME".to_owned()), + value: Some(serde_json::Value::String(pipeline.name)), + }); + param_list.push(PipelineParam { + name: Some("REPO_OWNER".to_owned()), + value: Some(serde_json::Value::String( + hook.get_repo_owner()?.into_owned(), + )), + }); + param_list.push(PipelineParam { + name: Some("REPO_NAME".to_owned()), + value: Some(serde_json::Value::String( + hook.get_repo_name()?.into_owned(), + )), + }); + let hook_repo_url = hook.get_repo_url()?; + param_list.push(PipelineParam { + name: Some("REPO_URL".to_owned()), + value: pipeline + .clone_uri + .map(|uri| serde_json::Value::String(uri)) + .or_else(|| Some(serde_json::Value::String(hook_repo_url.into_owned()))), + }); + param_list.push(PipelineParam { + name: Some("PULL_BASE_SHA".to_owned()), + value: Some(serde_json::Value::String( + hook.get_pull_base_sha()?.into_owned(), + )), + }); + param_list.push(PipelineParam { + name: Some("PULL_BASE_REF".to_owned()), + value: Some(serde_json::Value::String( + hook.get_pull_base_ref()?.into_owned(), + )), + }); + } + let pp = PostParams::default(); + let created_run = jobs.create(&pp, &pipeline.pipeline).await?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 590541d..2cea010 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,9 @@ use self::crd_pipeline_run::PipelineRun; use self::discovery::discover_matching_push_triggers; use self::discovery::discover_webhook_bridge_config; use self::gitea_client::GiteaClient; +use self::hook_push::HookPush; use self::in_repo_config::InRepoConfig; +use self::kubernetes::run_pipelines; use self::webhook::hook; use self::webhook::verify_signature; use kube::CustomResourceExt; @@ -32,9 +34,10 @@ mod discovery; mod gitea_client; mod hook_push; mod in_repo_config; +mod kubernetes; mod webhook; -const EXAMPLE_PIPELINE_RUN: &'static str = include_str!("../example_pipeline_run.json"); +const EXAMPLE_WEBHOOK_PAYLOAD: &'static str = include_str!("../example_webhook_payload.json"); #[tokio::main] async fn main() -> Result<(), Box> { @@ -59,7 +62,7 @@ async fn main() -> Result<(), Box> { .get_tree( "talexander", "webhook_bridge", - "6d3b9e9db82d7857baa114d89efcb1bf470f448d", + "b8444344c4821e87a894cd195f8bec39cd501f68", ) .await?; let in_repo_config = discover_webhook_bridge_config(&gitea, &repo_tree).await?; @@ -67,14 +70,9 @@ async fn main() -> Result<(), Box> { discover_matching_push_triggers(&gitea, &repo_tree, "refs/heads/main", &in_repo_config) .await?; - // let jobs: Api = Api::namespaced(kubernetes_client, "lighthouse"); - // let jobs: Api = Api::default_namespaced(kubernetes_client); - // tracing::info!("Using crd: {}", serde_json::to_string(&PipelineRun::crd())?); + let webhook_payload: HookPush = serde_json::from_str(EXAMPLE_WEBHOOK_PAYLOAD)?; - // let test_run: PipelineRun = serde_json::from_str(EXAMPLE_PIPELINE_RUN)?; - - // let pp = PostParams::default(); - // let created_run = jobs.create(&pp, &test_run).await?; + run_pipelines(webhook_payload, pipelines, kubernetes_client).await?; // let app = Router::new() // .route("/hook", post(hook))