diff --git a/docker/Dockerfile.linux.amd64 b/docker/Dockerfile.linux.amd64 index 2eb895d..da3ed88 100644 --- a/docker/Dockerfile.linux.amd64 +++ b/docker/Dockerfile.linux.amd64 @@ -1,18 +1,14 @@ -FROM --platform=linux/amd64 alpine:3.18 as base +FROM --platform=linux/amd64 alpine:latest as base WORKDIR /app -ENV HELM_EXPERIMENTAL_OCI=1 - RUN apk --no-cache add ca-certificates curl && \ - curl -LO "https://get.helm.sh/helm-v3.7.0-linux-amd64.tar.gz" && \ - tar -zxvf helm-v3.7.0-linux-amd64.tar.gz && \ + curl -LO "https://get.helm.sh/helm-v3.8.0-linux-amd64.tar.gz" && \ + tar -zxvf helm-v3.8.0-linux-amd64.tar.gz && \ mv linux-amd64/helm /usr/local/bin/helm && \ - rm -rf linux-amd64 helm-v3.7.0-linux-amd64.tar.gz && \ + rm -rf linux-amd64 helm-v3.8.0-linux-amd64.tar.gz && \ chmod +x /usr/local/bin/helm -RUN apk add --no-cache python3 +COPY ./drone-helm /app/ -COPY ./main.py /app/ - -CMD ["python", "/app/main.py"] +CMD ["/app/drone-helm"] \ No newline at end of file diff --git a/docker/Dockerfile.linux.arm64 b/docker/Dockerfile.linux.arm64 index 2d11141..cad1a5a 100644 --- a/docker/Dockerfile.linux.arm64 +++ b/docker/Dockerfile.linux.arm64 @@ -1,18 +1,14 @@ -FROM --platform=linux/arm64 alpine:3.18 as base +FROM --platform=linux/arm64 alpine:latest as base WORKDIR /app -ENV HELM_EXPERIMENTAL_OCI=1 - RUN apk --no-cache add ca-certificates curl && \ - curl -LO "https://get.helm.sh/helm-v3.7.0-linux-arm64.tar.gz" && \ - tar -zxvf helm-v3.7.0-linux-arm64.tar.gz && \ - mv linux-arm64/helm /usr/local/bin/helm && \ - rm -rf linux-arm64 helm-v3.7.0-linux-arm64.tar.gz && \ + curl -LO "https://get.helm.sh/helm-v3.8.0-linux-amd64.tar.gz" && \ + tar -zxvf helm-v3.8.0-linux-amd64.tar.gz && \ + mv linux-amd64/helm /usr/local/bin/helm && \ + rm -rf linux-amd64 helm-v3.8.0-linux-amd64.tar.gz && \ chmod +x /usr/local/bin/helm -RUN apk add --no-cache python3 +COPY ./drone-helm /app/ -COPY ./main.py /app/ - -CMD ["python", "/app/main.py"] +CMD ["/app/drone-helm"] \ No newline at end of file diff --git a/docker/Dockerfile.windows.amd64 b/docker/Dockerfile.windows.amd64 index 5f059cb..b7ee1d3 100644 --- a/docker/Dockerfile.windows.amd64 +++ b/docker/Dockerfile.windows.amd64 @@ -13,8 +13,6 @@ RUN Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointMa RUN choco install -y kubernetes-helm -RUN choco install -y python3 +COPY ./drone-helm.exe /app/ -COPY ./main.py /app/ - -ENTRYPOINT ["python", "C:\\app\\main.py"] \ No newline at end of file +CMD ["C:\\app\\drone-helm.exe"] diff --git a/docker/Dockerfile.windows.arm64 b/docker/Dockerfile.windows.arm64 deleted file mode 100644 index 5f059cb..0000000 --- a/docker/Dockerfile.windows.arm64 +++ /dev/null @@ -1,20 +0,0 @@ -FROM --platform=windows/amd64 mcr.microsoft.com/windows/servercore:ltsc2022 - -USER ContainerAdministrator - -WORKDIR C:\\app - -ENV HELM_EXPERIMENTAL_OCI=1 -ENV chocolateyVersion=1.4.0 - -SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] - -RUN Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) - -RUN choco install -y kubernetes-helm - -RUN choco install -y python3 - -COPY ./main.py /app/ - -ENTRYPOINT ["python", "C:\\app\\main.py"] \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..18fb13b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module drone/plugin/helm-chart-docker-registry + +go 1.21.5 diff --git a/main.go b/main.go new file mode 100644 index 0000000..e0aa5fb --- /dev/null +++ b/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "runtime" + "strings" +) + +func main() { + // get environment variables + registry := os.Getenv("PLUGIN_REGISTRY") + username := os.Getenv("PLUGIN_USERNAME") + token := os.Getenv("PLUGIN_TOKEN") + chartPath := os.Getenv("PLUGIN_CHART_PATH") + namespace := os.Getenv("PLUGIN_NAMESPACE") + + if (registry == "") || (username == "") || (token == "") || (namespace == "") { + fmt.Println("Missing required environment variables") + os.Exit(1) + } + + if chartPath == "" { + chartPath = "./" + } + + // get chart name + chartName, err := getChartName(chartPath) + if err != nil { + fmt.Println("Failed to get chart name") + os.Exit(1) + } + + chartName = strings.TrimSpace(chartName) + + // get chart version + chartVersion, err := getChartVersion(chartPath) + if err != nil { + fmt.Println("Failed to get chart version") + os.Exit(1) + } + + chartVersion = strings.TrimSpace(chartVersion) + + // cd into chart path + if err := os.Chdir(chartPath); err != nil { + fmt.Println(err) + os.Exit(1) + } + + // package chart + packageChartCmd := exec.Command("helm", "package", "--dependency-update", ".") + if err := packageChartCmd.Run(); err != nil { + fmt.Println(err) + os.Exit(1) + } + + // get chart tarball name + chartTarballName := fmt.Sprintf("%s-%s.tgz", chartName, chartVersion) + + fmt.Println("Chart packaged successfully - ", chartTarballName) + + // login to registry + loginCmd := exec.Command("helm", "registry", "login", registry, "--username", username, "--password", token) + if err := loginCmd.Run(); err != nil { + fmt.Println("Failed to login to registry") + os.Exit(1) + } + + fmt.Println("Logged in to registry successfully") + + // push chart + pushChartCmd := exec.Command("helm", "push", chartTarballName, fmt.Sprintf("oci://%s/%s", registry, namespace)) + if err := pushChartCmd.Run(); err != nil { + fmt.Println("Failed to push chart") + os.Exit(1) + } + + fmt.Println("Chart pushed successfully - ", registry, "/", namespace, "/", chartName) +} + +func getChartName(chartPath string) (string, error) { + // helm show chart chartPath | grep name | awk '{print $2}' + cmd1 := exec.Command("helm", "show", "chart", chartPath) + cmd2 := exec.Command("awk", "/name:/ {print $2}") + + // if os is windows + if runtime.GOOS == "windows" { + cmd1 = exec.Command("helm", "show", "chart", chartPath) + cmd2 = exec.Command("powershell", "-Command", "Select-String 'name:' | ForEach-Object { $_.Matches.Groups[1].Value }") + } + + var out1 bytes.Buffer + var out2 bytes.Buffer + + cmd1.Stdout = &out1 + cmd2.Stdin = &out1 + cmd2.Stdout = &out2 + + if err := cmd1.Run(); err != nil { + fmt.Println("Chart does not exist in the specified path") + return "", err + } + if err := cmd2.Run(); err != nil { + return "", err + } + + return out2.String(), nil +} + +func getChartVersion(chartPath string) (string, error) { + // helm show chart chartPath | grep version | awk '{print $2}' + cmd1 := exec.Command("helm", "show", "chart", chartPath) + cmd2 := exec.Command("awk", "/version:/ {print $2}") + + // if os is windows + if runtime.GOOS == "windows" { + cmd1 = exec.Command("helm", "show", "chart", chartPath) + cmd2 = exec.Command("powershell", "-Command", "Select-String 'version:' | ForEach-Object { $_.Matches.Groups[1].Value }") + } + + var out1 bytes.Buffer + var out2 bytes.Buffer + + cmd1.Stdout = &out1 + cmd2.Stdin = &out1 + cmd2.Stdout = &out2 + + if err := cmd1.Run(); err != nil { + fmt.Println("Chart does not exist in the specified path") + return "", err + } + if err := cmd2.Run(); err != nil { + return "", err + } + + return out2.String(), nil +} diff --git a/main.py b/main.py deleted file mode 100644 index f071100..0000000 --- a/main.py +++ /dev/null @@ -1,73 +0,0 @@ -# Plugin Name: Push OCI Chart to Registry -# Description: Pushes an Helm Chart to a Docker Registry - -import os -import subprocess - -# Environment Variables - - -def main_function(): - - CHART_NAME = os.getenv("PLUGIN_CHART_NAME") - CHART_VERSION = os.getenv("PLUGIN_CHART_VERSION", "1.0.0") - - DOCKER_REGISTRY = os.getenv( - "PLUGIN_DOCKER_REGISTRY", 'registry.hub.docker.com') - - DOCKER_USERNAME = os.getenv( - "PLUGIN_DOCKER_USERNAME") - DOCKER_PASSWORD = os.getenv( - "PLUGIN_DOCKER_PASSWORD") - - CHART_PATH = os.getenv("PLUGIN_CHART_PATH") - - DOCKER_NAMESPACE = os.getenv("PLUGIN_DOCKER_NAMESPACE") - - if (CHART_NAME is None or CHART_NAME == ""): - print("Please provide a chart name") - exit(1) - - if (DOCKER_USERNAME is None or DOCKER_PASSWORD is None or DOCKER_USERNAME == "" or DOCKER_PASSWORD == ""): - print("Please provide a username and a password") - exit(1) - - if (DOCKER_NAMESPACE is None or DOCKER_NAMESPACE == ""): - print("Please provide a namespace") - exit(1) - - if (CHART_PATH is not None): - os.chdir(CHART_PATH) - - try: - if (subprocess.run(["helm", "package", "--dependency-update", "."]).returncode != 0): - raise Exception("Failed to package chart!") - except: - print("Failed to package chart!") - exit(1) - - chart_filename = f"{CHART_NAME}-{CHART_VERSION}.tgz" - - try: - login_command = ['helm', 'registry', 'login', DOCKER_REGISTRY, - '-u', DOCKER_USERNAME, '-p', DOCKER_PASSWORD] - if (subprocess.run(login_command).returncode != 0): - raise Exception("Failed to login!") - except: - print("Failed to login!") - exit(1) - - try: - docker_push_command = ["helm", "push", chart_filename, - f"oci://{DOCKER_REGISTRY}/{DOCKER_NAMESPACE}"] - if (subprocess.run(docker_push_command).returncode != 0): - raise Exception("Failed to push chart!") - else: - print("Chart pushed successfully.") - except: - print("Failed to push chart!") - exit(1) - - -if __name__ == "__main__": - main_function() diff --git a/tests.py b/tests.py deleted file mode 100644 index c1565e0..0000000 --- a/tests.py +++ /dev/null @@ -1,97 +0,0 @@ -import unittest -from unittest.mock import patch -import os -from io import StringIO -from main import main_function - - -class TestPushOCIChartToRegistry(unittest.TestCase): - - def tearDown(self): - os.environ["PLUGIN_CHART_NAME"] = "" - os.environ["PLUGIN_CHART_VERSION"] = "" - os.environ["PLUGIN_DOCKER_REGISTRY"] = "" - os.environ["PLUGIN_DOCKER_USERNAME"] = "" - os.environ["PLUGIN_DOCKER_PASSWORD"] = "" - os.environ["PLUGIN_CHART_PATH"] = "" - - @patch("subprocess.run") - def test_successful_chart_push(self, mock_subprocess_run): - os.environ["PLUGIN_CHART_NAME"] = "mywebapp" - test_docker_username = os.environ["TEST_DOCKER_USERNAME"] - test_docker_password = os.environ["TEST_DOCKER_PASSWORD"] - os.environ["PLUGIN_DOCKER_USERNAME"] = test_docker_username - os.environ["PLUGIN_DOCKER_PASSWORD"] = test_docker_password - os.environ["PLUGIN_CHART_VERSION"] = "5.0.0" - os.environ["PLUGIN_DOCKER_REGISTRY"] = "registry.hub.docker.com" - os.environ["PLUGIN_CHART_PATH"] = "." - - mock_subprocess_run.return_value.returncode = 0 - - with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - main_function() - - mock_subprocess_run.assert_called_with( - ["helm", "push", "mywebapp-5.0.0.tgz", f"oci://registry.hub.docker.com/{test_docker_username}"]) - - expected_output = 'Chart pushed successfully.' - self.assertEqual(mock_stdout.getvalue().strip(), expected_output) - - @patch("subprocess.run") - def test_failed_to_package_chart(self, mock_subprocess_run): - os.environ["PLUGIN_CHART_NAME"] = "mywebapp" - test_docker_username = os.environ["TEST_DOCKER_USERNAME"] - test_docker_password = os.environ["TEST_DOCKER_PASSWORD"] - os.environ["PLUGIN_DOCKER_USERNAME"] = test_docker_username - os.environ["PLUGIN_DOCKER_PASSWORD"] = test_docker_password - os.environ["PLUGIN_CHART_PATH"] = "chart" - - mock_subprocess_run.return_value.returncode = 1 - - with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - with self.assertRaises(SystemExit) as context: - main_function() - - self.assertEqual(context.exception.code, 1) - - expected_output = 'Failed to package chart!' - self.assertEqual(mock_stdout.getvalue().strip(), expected_output) - - @patch("subprocess.run") - def test_chart_name_not_provided(self, mock_subprocess_run): - test_docker_username = os.environ["TEST_DOCKER_USERNAME"] - test_docker_password = os.environ["TEST_DOCKER_PASSWORD"] - os.environ["PLUGIN_DOCKER_USERNAME"] = test_docker_username - os.environ["PLUGIN_DOCKER_PASSWORD"] = test_docker_password - os.environ["PLUGIN_CHART_PATH"] = "chart" - - mock_subprocess_run.return_value.returncode = 0 - - with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - with self.assertRaises(SystemExit) as context: - main_function() - - self.assertEqual(context.exception.code, 1) - - expected_output = 'Please provide a chart name' - self.assertEqual(mock_stdout.getvalue().strip(), expected_output) - - @patch("subprocess.run") - def test_username_and_password_not_provided(self, mock_subprocess_run): - os.environ["PLUGIN_CHART_NAME"] = "mywebapp" - os.environ["PLUGIN_CHART_PATH"] = "chart" - - mock_subprocess_run.return_value.returncode = 0 - - with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - with self.assertRaises(SystemExit) as context: - main_function() - - self.assertEqual(context.exception.code, 1) - - expected_output = 'Please provide a username and a password' - self.assertEqual(mock_stdout.getvalue().strip(), expected_output) - - -if __name__ == '__main__': - unittest.main()