commit 1b67cbbb1db7bee6c7f4afb5d6b47c91d078f75d Author: Sangbum Kim Date: Sat Jan 4 04:08:44 2025 +0900 initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e4359d9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +target +.git +.idea +Dockerfile \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..609e9c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,209 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,windows,linux,macos,rust,rust-analyzer +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,intellij+all,windows,linux,macos,rust,rust-analyzer + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### rust-analyzer ### +# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules) +rust-project.json + + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,windows,linux,macos,rust,rust-analyzer + +/target/ +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b781102 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "kwd" +version = "0.1.0" +edition = "2021" + +[dependencies] +regex-lite = "^0.1" +path-clean="^1.0" +[profile.release] +lto = true # Enable Link Time Optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* +debug-assertions = false +overflow-checks = false +debug = false +rpath = false +incremental = false +opt-level = 3 + +[[bin]] +name = "copier" +path = "src/copier/main.rs" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8f9dcd8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +## +## Build +## +FROM rust:alpine AS build +LABEL org.opencontainers.image.authors="Sangbum Kim " + +# set the workdir and copy the source into it +WORKDIR /app +COPY . /app +ENV RUSTFLAGS='-Cpanic=abort -Clink-args=-Wl,-x,-s,--as-needed,--gc-sections,--build-id=none,--no-eh-frame-hdr' +RUN set -x && \ + cargo build --release + +## +## Deploy +## +FROM scratch +COPY --from=build /app/target/release/kwd /app/target/release/copier / +ENTRYPOINT ["/kwd"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a90a92e --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# kwd + +하나의 kaniko 이미지 빌드에 여러 tag를 붙일 때 destination작업을 진행 해 주는 툴 입니다. \ No newline at end of file diff --git a/contrib/poc.go b/contrib/poc.go new file mode 100644 index 0000000..4260ea7 --- /dev/null +++ b/contrib/poc.go @@ -0,0 +1,62 @@ +package main +import ( + "fmt" + "net/url" + "os" + "os/exec" + "regexp" + "strings" + "syscall" +) +var ( + tagFmt = regexp.MustCompile("^[a-zA-Z0-9_\\-.]+$") +) +func main() { + args := os.Args + kanikoBin := "/kaniko/executor" + if path, ok := os.LookupEnv("KANIKO_BIN"); ok { + _ = os.Unsetenv("KANIKO_BIN") + kanikoBin = path + } + repository := "" + if found, ok := os.LookupEnv("KANIKO_IMAGE_REPOSITORY"); ok { + _ = os.Unsetenv("KANIKO_IMAGE_REPOSITORY") + repository = strings.TrimSpace(found) + } + name := "" + if found, ok := os.LookupEnv("KANIKO_IMAGE_NAME"); ok { + _ = os.Unsetenv("KANIKO_IMAGE_NAME") + name = strings.TrimSpace(found) + } + tags := []string{"latest"} + if found, ok := os.LookupEnv("KANIKO_IMAGE_TAGS"); ok { + _ = os.Unsetenv("KANIKO_IMAGE_TAGS") + for _, rawTag := range strings.Split(found, ",") { + tag := strings.TrimSpace(rawTag) + if !tagFmt.MatchString(tag) { + continue + } else { + tags = append(tags, tag) + } + } + } + if image, _ := url.JoinPath(repository, name); len(image) > 0 { + for _, tag := range tags { + args = append(args, fmt.Sprintf("--destination=%s:%s", image, tag)) + } + } + if path, ok := os.LookupEnv(kanikoBin); ok { + _ = os.Unsetenv("KANIKO_BIN") + kanikoBin = path + } + if foundPath, err := exec.LookPath(kanikoBin); err != nil { + panic(fmt.Sprintf("failed to find path %s, %s", kanikoBin, err)) + } else { + kanikoBin = foundPath + } + args[0] = kanikoBin + fmt.Println(strings.Join(args, " ")) + if err := syscall.Exec(kanikoBin, args, os.Environ()); err != nil { + panic(fmt.Sprintf("kaniko exec failed: %s", err)) + } +} diff --git a/contrib/podman b/contrib/podman new file mode 100644 index 0000000..9cd73b2 --- /dev/null +++ b/contrib/podman @@ -0,0 +1,6 @@ +## First, initialise the manifest +#podman manifest create sangbumkim/kwd:v0.1.0 +## Build the image attaching them to the manifest +#podman build --platform linux/amd64,linux/arm64 --manifest sangbumkim/kwd:v0.1.0 . +## Finally publish the manifest +#podman manifest push sangbumkim/kwd:v0.1.0 diff --git a/src/copier/main.rs b/src/copier/main.rs new file mode 100644 index 0000000..18ea5e9 --- /dev/null +++ b/src/copier/main.rs @@ -0,0 +1,118 @@ +use path_clean::PathClean; +use std::env::args_os; +use std::ffi::OsString; +use std::fs::{File, FileTimes}; +use std::path::{Path, PathBuf}; +use std::{fs, path}; + +fn normalize_path(path_opt: Option) -> PathBuf { + let path = match path_opt { + Some(path) => PathBuf::from(path), + None => panic!("path not provided"), + }; + let path = match path::absolute(&path) { + Ok(path) => path.clean(), + Err(err) => panic!("failed to make absolute path({}): {}", path.display(), err), + }; + + path.clean() +} + +fn get_metadata(path: impl AsRef) -> Option<(fs::Permissions, fs::FileTimes)> { + let src_meta = match fs::metadata(path) { + Ok(meta) => meta, + Err(_) => return None, + }; + + let perm = src_meta.permissions(); + + let modified = match src_meta.modified() { + Ok(at) => at, + Err(_) => return None, + }; + + let accessed = match src_meta.accessed() { + Ok(at) => at, + Err(_) => return None, + }; + + let tm = FileTimes::new(); + tm.set_modified(modified); + tm.set_accessed(accessed); + + Some((perm, tm)) +} +fn main() { + let mut args = args_os().skip(1); + + let src = normalize_path(args.next()); + let dst = normalize_path(args.next()); + + + let src = if src.eq(&dst) { + panic!("source and destination is same!") + } else if !src.exists() { + panic!("source({}) not exists!", src.display()) + } else if src.is_file() { + src + } else if src.is_symlink() { + match fs::read_link(&src) { + Ok(src) => normalize_path(Some(src.as_os_str().to_os_string())), + Err(err) => panic!("failed to read link {}: {}", src.display(), err), + } + } else { + panic!("source({}) is not a file!", src.display()) + }; + + let src_filename = match src.file_name() { + None => panic!("source({}) doesn't a filename!", src.display()), + Some(name) => name, + }; + + let (src_perm, src_tm) = match get_metadata(&src) { + Some((perm, tm)) => (perm, tm), + None => panic!("failed to read metadata"), + }; + + let dst = if !dst.exists() { + if let Some(parent) = &dst.parent() { + if let Err(err) = fs::create_dir_all(&parent) { + panic!( + "failed to create destination directory({}): {}", + parent.display(), + err + ); + } + } + dst + } else if !dst.is_dir() { + dst.join(src_filename) + } else { + dst + }; + + eprint!("{} -> {}: ", src.display(),dst.display()); + + if let Err(err) = fs::copy(&src, &dst) { + panic!( + "failed to copy: {} -> {}, {}", + &src.display(), + &dst.display(), + err + ); + } + + let dst_file = match File::open(dst) { + Ok(file) => file, + Err(err) => panic!("failed to open file: {}", err), + }; + + if let Err(err) = dst_file.set_permissions(src_perm) { + panic!("failed to set permissions: {}", err); + } + if let Err(err) = dst_file.set_times(src_tm) { + panic!("failed to set file times: {}", err); + } + + eprintln!("OK") +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..39355e6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,61 @@ +use regex_lite::Regex; +use std::collections::HashSet; +use std::env; +use std::os::unix::process::CommandExt; +use std::process::Command; + +fn main() { + let mut kaniko_bin = "/kaniko/executor".to_string(); + if let Ok(value) = env::var("KANIKO_BIN") { + env::remove_var("KANIKO_BIN"); + kaniko_bin = value.trim().to_string(); + } + + let mut repository = "".to_string(); + if let Ok(value) = env::var("KANIKO_IMAGE_REPOSITORY") { + env::remove_var("KANIKO_IMAGE_REPOSITORY"); + repository = value.trim().to_string() + } + + let mut name = "".to_string(); + if let Ok(value) = env::var("KANIKO_IMAGE_NAME") { + env::remove_var("KANIKO_IMAGE_NAME"); + name = value.trim().to_string(); + } + + let mut tags: HashSet = HashSet::new(); + + tags.insert("latest".to_string()); + + if let Ok(value) = env::var("KANIKO_IMAGE_TAGS") { + env::remove_var("KANIKO_IMAGE_TAGS"); + let pattern = Regex::new(r"^[a-zA-Z0-9_\\-.]+$").unwrap(); + for tag in value.split(',') { + let tag = tag.trim(); + if !pattern.is_match(tag) { + continue; + } + tags.insert(tag.to_string()); + } + } + + let image = match (repository.chars().last(), name.chars().last()) { + (Some('/'), Some('/')) => format!("{}{}", repository.trim_end_matches("/"), name), + (Some('/'), Some(_)) => format!("{}{}", repository, name), + (Some(_), Some('/')) => format!("{}{}", repository, name), + (Some(_), Some(_)) => format!("{}/{}", repository, name), + (None, Some(_)) => name, + (Some(_), None) => repository, + (None, None) => "".to_string(), + }; + + let mut args: Vec<_> = env::args().skip(1).collect(); + if !image.is_empty() { + for tag in tags { + args.push(format!("--destination={}:{}", image, tag)); + } + } + + let err = Command::new(kaniko_bin).args(&args).exec(); + panic!("failed to execv:{}", err); +}