Repository: GetSimpl/cloudlift (language: python)

Issue:
Tasks have IAM Roles to allow access to other AWS services
Create/Update of service, will create a service specific role and
attach it to every task via the task definition. By default, the role
has no permissions.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release_packages.yml b/.github/workflows/release_packages.yml
new file mode 100644
index 00000000..ab1a657c
--- /dev/null
+++ b/.github/workflows/release_packages.yml
@@ -0,0 +1,192 @@
+name: Build and Release PyInstaller Packages
+
+on:
+  push:
+    tags:
+      - "v*.*.*"
+
+env:
+  PYTHON_VERSION: "3.11"
+
+jobs:
+  build:
+    strategy:
+      matrix:
+        include:
+          - os: macos-latest
+            build_os: darwin
+          - os: ubuntu-latest
+            build_os: linux
+          - os: ubuntu-latest
+            build_os: alpine
+    runs-on:
+      - ${{ matrix.os }}
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "${{ env.PYTHON_VERSION }}"
+
+      - name: Set Variables
+        id: set_variables
+        run: |
+          echo "version=$(cat cloudlift/version/__init__.py | cut -d'=' -f2 | tr -d "'" | tr -d ' ')" >> $GITHUB_ENV
+          package_name=$(python scripts/build.py --package-name --os ${{ matrix.build_os }})
+          echo "Package Name: $package_name"
+          echo "package_name=$package_name" >> $GITHUB_ENV
+
+      - name: Set up Docker Buildx
+        if: matrix.os == 'ubuntu-latest'
+        uses: docker/setup-buildx-action@v3
+
+      - name: Build Docker Image
+        if: matrix.os == 'ubuntu-latest'
+        uses: docker/build-push-action@v5
+        with:
+          context: scripts/dockerfiles/
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+          file: scripts/dockerfiles/dockerfile.${{ matrix.build_os }}
+          tags: cloudlift-builder-${{ matrix.build_os }}:latest
+          load: true
+
+      - name: Build Cloudlift Package
+        run: python scripts/build.py --os ${{ matrix.build_os }}
+
+      - name: Upload Artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: "${{ env.package_name }}"
+          path: "/tmp/${{ env.package_name }}.tar.gz"
+
+  add_checksums:
+    needs: build
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Download Artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: "${{ github.workspace }}/artifacts"
+
+      - name: Generate Checksums
+        run: |
+          find "${{ github.workspace }}/artifacts" -type f -name "*.tar.gz" | while read -r file; do
+              sha=$(sha256sum "$file" | cut -d' ' -f1)
+              echo "$(basename "$file"): $sha" >> ${{ github.workspace }}/checksums.txt
+          done
+
+      - name: Upload Checksums
+        uses: actions/upload-artifact@v4
+        with:
+          name: checksums
+          path: ${{ github.workspace }}/checksums.txt
+
+  test:
+    needs: add_checksums
+    strategy:
+      matrix:
+        include:
+          - os: macos-latest
+            build_os: darwin
+          - os: ubuntu-latest
+            build_os: linux
+          - os: ubuntu-latest
+            build_os: alpine
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "${{ env.PYTHON_VERSION }}"
+
+      - name: Set Variables
+        id: set_variables
+        run: |
+          echo "version=$(cat cloudlift/version/__init__.py | cut -d'=' -f2 | tr -d "'" | tr -d ' ')" >> $GITHUB_ENV
+          package_name=$(python scripts/build.py --package-name --os ${{ matrix.build_os }})
+          echo "Package Name: $package_name"
+          echo "package_name=$package_name" >> $GITHUB_ENV
+
+      - name: Download Artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: "${{ github.workspace }}/artifacts"
+
+      - name: Run Tests
+        run: |
+          python scripts/build.py --os ${{ matrix.build_os }}  --only-test --package-dir ${{ github.workspace }}/artifacts/${{ env.package_name }}
+
+  test_installer:
+    needs: add_checksums
+    strategy:
+      matrix:
+        include:
+          - os: macos-latest
+          - os: ubuntu-latest
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "${{ env.PYTHON_VERSION }}"
+
+      - name: Download Artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: "${{ github.workspace }}/artifacts"
+
+
+      - name: Test Installer
+        run: |
+          installer_test_path=/tmp/installer-test
+          installer_test_artifacts_path=$installer_test_path/${{ github.ref_name }}
+          mkdir -p $installer_test_path
+          mkdir -p $installer_test_artifacts_path
+          find ./artifacts -name '*.tar.gz' -exec mv {} $installer_test_artifacts_path \;
+          find ./artifacts -name 'checksums.txt' -exec mv {} $installer_test_artifacts_path \;
+          cp -r scripts/* /tmp/installer-test
+          cd /tmp/installer-test
+          bash test_installer.sh ${{ github.ref_name }} 
+
+  release:
+    runs-on: ubuntu-latest
+    if: startsWith(github.ref, 'refs/tags/')
+    needs: test
+
+    steps:
+      - name: Get and Set Tag
+        id: set_tag
+        run: |
+          tag=$(echo $GITHUB_REF | cut -d'/' -f3)
+          echo "tag=$tag" >> $GITHUB_ENV
+
+      - name: Download Artifacts
+        uses: actions/download-artifact@v4
+        with:
+          path: "${{ github.workspace }}/artifacts"
+
+      - name: Create Release
+        uses: softprops/action-gh-release@v2
+        if: startsWith(github.ref, 'refs/tags/')
+        with:
+          files: |
+            ${{ github.workspace }}/artifacts/**/*.tar.gz
+            ${{ github.workspace }}/artifacts/checksums/checksums.txt
+          tag_name: ${{ env.tag }}
+          name: "Release ${{ env.tag }}"
+          draft: false
+          prerelease: false
+          token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index af30a958..11b0870d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@ dist/
 venv/
 metastore_db/
 .eggs
-.idea
\ No newline at end of file
+.idea
+*.spec
\ No newline at end of file
diff --git a/Makefile b/Makefile
index ef40dd3e..3ee964dc 100644
--- a/Makefile
+++ b/Makefile
@@ -25,3 +25,4 @@ package-upload: package
 	python3 -m twine upload dist/*
     
 
+.PHONY: clean test-template test-integration package package-test-upload install-test-package package-upload
diff --git a/README.md b/README.md
index 5e13f83b..c12164d8 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,20 @@ in AWS.
 
 ## Installing cloudlift
 
+### Using Script
+
+```sh
+sh -c "$(curl -fsSL https://raw.githubusercontent.com/GetSimpl/cloudlift/master/scripts/installer.sh)"
+```
+
+Note: Append `sudo` to the above command if you are not running as root user. For running in a docker container, no need to append `sudo`.
+
+Currently supported platforms using the above script are -
+
+- `linux-x86_64`
+- `alpine-x86_64`
+- `macos-arm64`
+
 ### 1. Pre-requisites
 
 - pip
diff --git a/bin/cloudlift b/bin/cloudlift
new file mode 100644
index 00000000..2064c1f6
--- /dev/null
+++ b/bin/cloudlift
@@ -0,0 +1,5 @@
+from cloudlift import cli
+import sys
+
+if __name__ == '__main__':
+    sys.exit(cli())
diff --git a/requirements.dev.txt b/requirements.dev.txt
new file mode 100644
index 00000000..bb8d39e9
--- /dev/null
+++ b/requirements.dev.txt
@@ -0,0 +1,2 @@
+moto==1.3.7
+pytest==4.0.0
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 9e13cc56..6d7ebc91 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,8 +12,6 @@ futures==3.1.1
 idna==2.6
 jmespath==0.9.3
 jsonschema==2.6.0
-moto==1.3.7
-pytest==4.0.0
 python-dateutil==2.8.2
 requests>=2.20.0
 six==1.10.0
@@ -21,4 +19,4 @@ stringcase==1.0.6
 terminaltables==3.1.0
 troposphere==4.5.2
 awacs==2.4.0
-botocore==1.34.111  
+botocore==1.34.111
\ No newline at end of file
diff --git a/scripts/build.py b/scripts/build.py
new file mode 100755
index 00000000..143120d0
--- /dev/null
+++ b/scripts/build.py
@@ -0,0 +1,574 @@
+import argparse
+import os
+import platform
+import shlex
+import shutil
+import subprocess
+import sys
+import tarfile
+from pathlib import Path
+
+DOCKERFILES = {
+    "linux": "dockerfile.linux",
+    "alpine": "dockerfile.alpine",
+}
+PYINSTALLER_VERSION = os.getenv("PYINSTALLER_VERSION", "6.10.0")
+
+# test distros are mainly used for testing the package
+# while developing on the local machine. these are not required
+# for the CI/CD pipeline as CI/CD pipeline will run tests separately.
+# to test the packages while building, use the --test flag. otherwise use --only-test flag to only run tests.
+TEST_DISTROS = {
+    "linux": {
+        "ubuntu": ["24.04", "22.04", "20.04", "18.04", "16.04", "14.04"],
+        "debian": ["12", "11", "10", "9", "8"],
+        "amazonlinux": ["2023", "2", "1"],
+        "fedora": ["40", "39", "38"],
+    },
+    "alpine": {
+        "alpine": [
+            "3.20",
+            "3.19",
+            "3.18",
+            "3.17",
+            "3.16",
+            "3.15",
+            "3.14",
+            "3.13",
+            "3.12",
+            "3.11",
+        ],
+    },
+}
+
+
+def BuildError(Exception):
+    pass
+
+
+class Builder:
+    def __init__(
+        self, work_dir: Path, os_name: str, arch: str, use_docker: bool
+    ) -> None:
+        self.work_dir = work_dir
+        self.os_name = os_name
+        self.arch = arch
+        self.use_docker = use_docker
+        self.docker_image = f"cloudlift-builder-{self.os_name}"
+        self.docker_container = f"cloudlift-builder-{self.os_name}"
+        self.package_name = self._get_package_name()
+        self.python = "python3"
+
+    @staticmethod
+    def print_info(message: str):
+        print(f"\033[0;32m[INFO]\033[0m {message}")
+
+    @staticmethod
+    def print_error(message: str):
+        print(f"\033[0;31m[ERROR]\033[0m {message}")
+        raise BuildError(message)
+
+    @staticmethod
+    def print_success(message: str):
+        print(f"\033[0;32m[SUCCESS]\033[0m {message}")
+
+    @staticmethod
+    def print_warning(message: str):
+        print(f"\033[0;33m[WARNING]\033[0m {message}")
+
+    def _get_package_name(self) -> str:
+        version_file = os.path.join(
+            self.work_dir, "cloudlift", "version", "__init__.py"
+        )
+        version = None
+
+        with open(version_file, encoding="utf-8") as f:
+            for line in f:
+                if line.startswith("VERSION"):
+                    version = line.split("=")[1].strip().strip("'")
+
+        package_name = f"cloudlift-{self.os_name}-{self.arch}-v{version}"
+        return package_name
+
+    def _check_for_binary(self, binary: str) -> None:
+        if not shutil.which(binary):
+            self.print_error(f"Could not find {binary}")
+
+    def cleanup(self):
+        self.print_info("Cleaning up")
+
+        if os.path.exists(self.work_dir / "build"):
+            shutil.rmtree(self.work_dir / "build")
+
+        if os.path.exists(self.work_dir / "venv"):
+            shutil.rmtree(self.work_dir / "venv")
+
+        spec_files = list(self.work_dir.glob("*.spec"))
+        for spec_file in spec_files:
+            os.remove(spec_file)
+
+        # remove .egg-info
+        egg_info = list(self.work_dir.glob("*.egg-info"))
+        for egg in egg_info:
+            shutil.rmtree(egg)
+
+    def preflight_check(self):
+        self.print_info("Running preflight checks")
+
+        if not os.path.exists(self.work_dir / "cloudlift" / "version" / "__init__.py"):
+            self.print_error("Could not find version/__init__.py")
+
+        packages = [self.python]
+        for package in packages:
+            self._check_for_binary(package)
+
+        if self.use_docker:
+            self._check_for_binary("docker")
+
+        self.print_info("Preflight checks passed")
+
+    def _check_for_docker_image(self) -> bool:
+        cmd = ["docker", "images", "--format", "{{.Repository}}"]
+        self.print_info(" ".join(cmd))
+        output = subprocess.run(cmd, check=True, capture_output=True, text=True)
+
+        if self.docker_image not in output.stdout:
+            self.print_info(f"Could not find Docker image {self.docker_image}")
+            return False
+
+        return True
+
+    def create_docker_image(self):
+        if not self.use_docker:
+            return
+
+        dockerfile = DOCKERFILES.get(self.os_name)
+        if not dockerfile:
+            self.print_error(f"Could not find Dockerfile for {self.os_name}")
+
+        dockerfile_path = os.path.join(self.work_dir, "scripts/dockerfiles", dockerfile)
+
+        self.print_info(f"Building Docker image for {self.os_name}")
+        docker_build_cmd = [
+            "docker",
+            "build",
+            "-t",
+            self.docker_image,
+            "--build-arg",
+            f"PYINSTALLER_VERSION={PYINSTALLER_VERSION}",
+            "-f",
+            dockerfile_path,
+            ".",
+        ]
+
+        self.print_info(" ".join(docker_build_cmd))
+        subprocess.run(docker_build_cmd, check=True)
+
+    def create_docker_container(self):
+        if not self.use_docker:
+            return
+
+        # check if the docker image exists
+        if not self._check_for_docker_image():
+            self.print_error(
+                f"Could not find Docker image {self.docker_image}; please build the image first"
+            )
+
+        # check if container already exists
+        cmd = ["docker", "ps", "-a", "--format", "{{.Names}}"]
+        self.print_info(" ".join(cmd))
+        output = subprocess.run(cmd, check=True, capture_output=True, text=True)
+        self.print_info(output.stdout)
+
+        if self.docker_container in output.stdout:
+            self.print_info(f"Container {self.docker_container} already exists")
+            self.delete_container()
+
+        self.print_info(
+            f"Creating Docker container for {self.os_name} in detached mode"
+        )
+
+        docker_platform = "linux/arm64" if self.arch == "arm64" else "linux/amd64"
+
+        host_dir_abs = os.path.abspath(self.work_dir).strip()
+        self.print_info(f"Mounting {host_dir_abs} to /app in container")
+
+        docker_run_cmd = [
+            "docker",
+            "run",
+            "-d",
+            "-i",
+            "-v",
+            f"{shlex.quote(host_dir_abs)}:/app",
+            f"--platform={docker_platform}",
+            "--name",
+            self.docker_container,
+            self.docker_image,
+        ]
+
+        self.print_info(subprocess.list2cmdline(docker_run_cmd))
+        subprocess.run(docker_run_cmd, check=True)
+
+    def delete_container(self):
+        if not self.use_docker:
+            return
+
+        # check if container already exists
+        cmd = ["docker", "ps", "-a", "--format", "{{.Names}}"]
+        self.print_info(" ".join(cmd))
+        output = subprocess.run(cmd, check=True, capture_output=True, text=True)
+
+        if self.docker_container not in output.stdout:
+            self.print_info(f"Container {self.docker_container} does not exist")
+            return
+
+        self.print_info(f"Stopping and removing container {self.docker_image}")
+        docker_rm_cmd = ["docker", "rm", "-f", self.docker_image]
+
+        self.print_info(" ".join(docker_rm_cmd))
+        subprocess.run(docker_rm_cmd, check=True)
+
+    def setup_venv(self):
+        if self.os_name != "darwin":
+            self.print_info("Virtual env is not required for non-darwin platforms")
+            return
+
+        self.print_info("Setting up virtual environment")
+        venv_path = os.path.join(self.work_dir, "venv")
+
+        # if venv exists, remove it
+        if os.path.exists(venv_path):
+            shutil.rmtree(venv_path)
+
+        venv_python = f"{venv_path}/bin/python"
+        os.environ["VIRTUAL_ENV"] = str(venv_path)
+        os.environ["PATH"] = f"{venv_path}/bin:{os.environ['PATH']}"
+
+        sys.prefix = str(venv_path)
+        sys.executable = venv_python
+
+        if sys.prefix != str(venv_path):
+            self.print_error("Could not set up virtual environment")
+
+        venv_cmd = [self.python, "-m", "venv", venv_path]
+        self.print_info(" ".join(venv_cmd))
+        subprocess.run(venv_cmd, check=True)
+
+        # set python to venv python path for darwin
+        # as python will be used from venv
+        if not self.use_docker:
+            self.python = venv_python
+
+    def install_requirements(self):
+        self.print_info("Installing requirements")
+
+        cmd = [
+            self.python,
+            "-m",
+            "pip",
+            "install",
+            ".",
+        ]
+        shell_cmd = "/bin/sh" if self.os_name == "alpine" else "/bin/bash"
+
+        try:
+            if self.use_docker:
+                # run inside docker container
+                docker_exec_cmd = [
+                    "docker",
+                    "exec",
+                    "-i",
+                    self.docker_container,
+                    shell_cmd,
+                    "-c",
+                    "cd /app && " + subprocess.list2cmdline(cmd),
+                ]
+
+                self.print_info(subprocess.list2cmdline(docker_exec_cmd))
+                subprocess.run(docker_exec_cmd, check=True)
+            else:
+                self.print_info(subprocess.list2cmdline(cmd))
+                subprocess.run(cmd, check=True, cwd=self.work_dir)
+        except subprocess.CalledProcessError:
+            self.print_error("Could not install requirements")
+
+        if self.os_name == "darwin":
+            self._install_pyinstaller()
+
+    def _install_pyinstaller(self) -> None:
+        self.print_info(f"Installing pyinstaller {PYINSTALLER_VERSION}")
+
+        pyinstaller_cmd = [
+            self.python,
+            "-m",
+            "pip",
+            "install",
+            f"pyinstaller=={PYINSTALLER_VERSION}",
+        ]
+        self.print_info(subprocess.list2cmdline(pyinstaller_cmd))
+        subprocess.run(pyinstaller_cmd, check=True)
+
+    def build_package(self):
+        self.print_info("Building package")
+        pyinstaller_args = [
+            "pyinstaller",
+            "--onedir",
+            "--clean",
+            "--noconfirm",
+            "--nowindow",
+            "--name=cloudlift",
+            "--paths=cloudlift",
+            "--add-data=LICENSE:.",
+            "--add-data=README.md:.",
+            "--exclude-module=pyinstaller",
+            "bin/cloudlift",
+        ]
+        shell_cmd = "/bin/sh" if self.os_name == "alpine" else "/bin/bash"
+
+        if self.use_docker:
+            docker_exec_cmd = [
+                "docker",
+                "exec",
+                "-i",
+                self.docker_container,
+                shell_cmd,
+                "-c",
+                "cd /app && " + subprocess.list2cmdline(pyinstaller_args),
+            ]
+
+            self.print_info(subprocess.list2cmdline(docker_exec_cmd))
+            subprocess.run(docker_exec_cmd, check=True)
+        else:
+            self.print_info(subprocess.list2cmdline(pyinstaller_args))
+            subprocess.run(pyinstaller_args, check=True, cwd=self.work_dir)
+
+    def package(self):
+        self.print_info(
+            f"Packaging Cloudlift for {self.os_name} to {self.package_name}"
+        )
+
+        try:
+            temp_dir = os.path.join("/tmp")
+            temp_package_path = os.path.join(temp_dir, f"{self.package_name}.tar.gz")
+
+            # create the package in temp directory
+            with tarfile.open(temp_package_path, "w:gz") as tar:
+                cloudlift_dir = os.path.join(self.work_dir, "dist", "cloudlift")
+                tar.add(cloudlift_dir, arcname=self.package_name)
+
+            self.print_info(f"Package created at {temp_package_path}")
+
+        except PermissionError as e:
+            self.print_error(f"Permission error while creating package: {str(e)}")
+        except OSError as e:
+            self.print_error(f"OS error while creating package: {str(e)}")
+        except Exception as e:
+            self.print_error(f"Unexpected error while creating package: {str(e)}")
+
+    def test_package(self, package_path: str) -> None:
+        self.print_info("Testing package")
+
+        # if not package_path:
+        if package_path.startswith("/tmp"):
+            # copy the package to work_dir/dist as I was facing issues with mounting
+            # /tmp/<package_name>.tar.gz to /app. when I mounted
+            # /app was empty in the container. this is a workaround for now.
+
+            tmp_package_path = os.path.join("/tmp", f"{self.package_name}.tar.gz")
+            package_path = os.path.join(
+                self.work_dir, "dist", f"{self.package_name}.tar.gz"
+            )
+            self.print_info(
+                f"Copying package from {tmp_package_path} to {package_path}"
+            )
+
+            # create package_path directory if it does not exist
+            if not os.path.exists(os.path.dirname(package_path)):
+                os.makedirs(os.path.dirname(package_path))
+            shutil.copy(tmp_package_path, os.path.dirname(package_path))
+
+        if not os.path.exists(package_path):
+            self.print_error(f"Could not find package at {package_path}")
+
+        cloudlift_extracted_dir = os.path.join(
+            os.path.dirname(package_path), self.package_name
+        )
+        with tarfile.open(package_path, "r:gz") as tar:
+            tar.extractall(path=os.path.dirname(package_path))
+
+        if not os.path.exists(cloudlift_extracted_dir):
+            self.print_error("Could not extract package")
+
+        # run tests
+        if self.use_docker:
+            self._test_on_containers(cloudlift_extracted_dir)
+        else:
+            self._test_on_local(cloudlift_extracted_dir)
+
+    def _test_on_local(self, cloudlift_extracted_dir):
+        self.print_info("Testing on local machine")
+
+        test_cmd = [
+            # "./dist/cloudlift/cloudlift",
+            os.path.join(cloudlift_extracted_dir, "cloudlift"),
+            "--version",
+        ]
+
+        self.print_info(subprocess.list2cmdline(test_cmd))
+        subprocess.run(test_cmd, check=True, cwd=self.work_dir)
+
+    def _test_on_containers(self, cloudlift_extracted_dir):
+        distros = TEST_DISTROS.get(self.os_name)
+        if not distros:
+            self.print_error(f"Could not find distros for {self.os_name}")
+
+        is_failed = False
+        for distro, versions in distros.items():
+            for version in versions:
+                response = self._test_on_container(
+                    self.os_name, distro, version, cloudlift_extracted_dir
+                )
+                if not response:
+                    is_failed = True
+
+        if is_failed:
+            self.print_error(f"Some tests failed for {self.os_name}")
+
+    def _test_on_container(
+        self, os_name, distro, version, cloudlift_extracted_dir
+    ) -> bool:
+        docker_platform = "linux/arm64" if self.arch == "arm64" else "linux/amd64"
+        # host_dir_abs = os.path.abspath(self.work_dir).strip()
+        shell_cmd = "/bin/sh" if self.os_name == "alpine" else "/bin/bash"
+
+        cmd = [
+            "docker",
+            "run",
+            "--rm",
+            "-i",
+            "-v",
+            f"{shlex.quote(cloudlift_extracted_dir)}:/app",
+            f"--platform={docker_platform}",
+            f"{distro}:{version}",
+            shell_cmd,
+            "-c",
+            "cd /app && ./cloudlift --version",
+        ]
+
+        output = subprocess.run(
+            cmd, capture_output=True, text=True, timeout=60, check=False
+        )
+
+        if output.returncode == 0:
+            self.print_success(f"Test passed for {distro}:{version}")
+            return True
+        else:
+            self.print_warning(f"Test failed for {distro}:{version}")
+            return False
+
+    def build(
+        self, test_package: bool, only_test: bool, package_dir: str, build_image: bool
+    ) -> None:
+        package_path = os.path.join(str(package_dir), f"{self.package_name}.tar.gz")
+
+        if only_test:
+            self.print_info("Only running tests on the package")
+            self.test_package(package_path)
+            return
+
+        self.preflight_check()
+
+        if build_image:
+            self.create_docker_image()
+
+        self.create_docker_container()
+        self.setup_venv()
+        self.install_requirements()
+        self.build_package()
+        self.package()
+
+        if test_package:
+            self.test_package(package_path)
+
+
+def get_os_name() -> str:
+    os_name = platform.system().lower()
+    if os_name == "linux":
+        if os.path.exists("/etc/alpine-release"):
+            return "alpine"
+
+    return os_name
+
+
+def get_arch() -> str:
+    return platform.machine()
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Build Cloudlift package")
+    parser.add_argument(
+        "--os",
+        type=str,
+        default=get_os_name(),
+        help="Operating system to build package for",
+    )
+    parser.add_argument(
+        "--arch",
+        type=str,
+        default=get_arch(),
+        help="Architecture to build package for",
+    )
+    parser.add_argument(
+        "--package-name",
+        action="store_true",
+        help="Print package name and exit",
+    )
+    parser.add_argument(
+        "--test",
+        action="store_true",
+        help="Run tests on the package; mainly used for local testing while developing",
+    )
+    parser.add_argument(
+        "--only-test",
+        action="store_true",
+        help="Only run tests on the package; mainly used in CI/CD pipeline. This will not build the package. Also pass the path to the package using --package-dir, which is optional",
+    )
+    parser.add_argument(
+        "--package-dir",
+        type=str,
+        default="/tmp",
+        help="Directory to the package to run tests on; used in CI/CD pipeline",
+    )
+    parser.add_argument(
+        "--build-image",
+        action="store_true",
+        help="Build base Docker image required for building Cloudlift packages with PyInstaller for the specified OS",
+    )
+    args = parser.parse_args()
+
+    work_dir = Path(__file__).resolve().parent.parent
+    use_docker = False
+
+    # handle macOS or darwin as os
+    if args.os.lower() in ["macos", "darwin"]:
+        args.os = "darwin"
+
+    if args.os in ["linux", "alpine"]:
+        use_docker = True
+
+    builder = Builder(work_dir, args.os, args.arch, use_docker)
+
+    if args.package_name:
+        sys.stdout.write(builder.package_name)
+        sys.exit(0)
+
+    builder.build(
+        test_package=args.test,
+        only_test=args.only_test,
+        package_dir=args.package_dir,
+        build_image=args.build_image,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/dockerfiles/dockerfile.alpine b/scripts/dockerfiles/dockerfile.alpine
new file mode 100644
index 00000000..6b7b6387
--- /dev/null
+++ b/scripts/dockerfiles/dockerfile.alpine
@@ -0,0 +1,34 @@
+FROM --platform=linux/amd64 python:3.9-alpine3.12
+
+# install system dependencies
+RUN apk add --no-cache \
+    bash \
+    build-base \
+    ca-certificates \
+    curl \
+    gcc \
+    git \
+    libc-dev \
+    libffi-dev \
+    libgcc \
+    make \
+    musl-dev \
+    openssl \
+    openssl-dev \
+    zlib-dev
+
+# set pyinstaller version
+ARG PYINSTALLER_VERSION=6.10.0
+ENV PYINSTALLER_VERSION=$PYINSTALLER_VERSION
+
+# install pyinstaller
+RUN set -ex \
+    && git clone --branch "v$PYINSTALLER_VERSION" --depth 1 https://github.com/pyinstaller/pyinstaller.git /tmp/pyinstaller \
+    && cd /tmp/pyinstaller/bootloader \
+    && python3 ./waf configure all \
+    && pip install .. \
+    && pyinstaller --version \
+    && rm -rf /tmp/pyinstaller
+
+# set the default command
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/scripts/dockerfiles/dockerfile.linux b/scripts/dockerfiles/dockerfile.linux
new file mode 100644
index 00000000..d57cb009
--- /dev/null
+++ b/scripts/dockerfiles/dockerfile.linux
@@ -0,0 +1,76 @@
+FROM --platform=linux/amd64 ubuntu:14.04
+
+# install system dependencies
+RUN apt-get update && apt-get install -y \
+    build-essential \
+    zlib1g-dev \
+    libncurses5-dev \
+    libgdbm-dev \
+    libnss3-dev \
+    libssl-dev \
+    libreadline-dev \
+    libffi-dev \
+    wget \
+    curl \
+    llvm \
+    libncursesw5-dev \
+    xz-utils \
+    tk-dev \
+    liblzma-dev \
+    libsqlite3-dev \
+    libbz2-dev \
+    libdb-dev \
+    libuuid1 \
+    uuid-dev \
+    gcc \
+    git \
+    libc-dev \
+    make \
+    openssl \
+    && apt-get clean \
+    && rm -rf /var/lib/apt/lists/*
+
+# install OpenSSL 1.1.1
+RUN wget https://www.openssl.org/source/openssl-1.1.1k.tar.gz && \
+    tar -xvf openssl-1.1.1k.tar.gz && \
+    cd openssl-1.1.1k && \
+    ./config --prefix=/usr/local/ssl --openssldir=/usr/local/ssl shared zlib && \
+    make -j $(nproc) && \
+    make install && \
+    cd .. && \
+    rm -rf openssl-1.1.1k.tar.gz openssl-1.1.1k
+
+# download and install Python 3.9.20
+RUN wget https://www.python.org/ftp/python/3.9.20/Python-3.9.20.tgz && \
+    tar -xf Python-3.9.20.tgz && \
+    cd Python-3.9.20 && \
+    ./configure \
+    --enable-optimizations \
+    --with-openssl=/usr/local/ssl \
+    --with-ensurepip=install \
+    --enable-shared \
+    LDFLAGS="-L/usr/local/ssl/lib -Wl,-rpath,/usr/local/ssl/lib" \
+    CPPFLAGS="-I/usr/local/ssl/include" && \
+    make install && \
+    cd .. && \
+    rm -rf Python-3.9.20.tgz Python-3.9.20
+
+# update library cache
+RUN ldconfig
+
+# upgrade pip
+RUN python3.9 -m pip install --upgrade pip
+
+# set pyinstaller version
+ARG PYINSTALLER_VERSION=6.10.0
+ENV PYINSTALLER_VERSION=$PYINSTALLER_VERSION
+
+# install pyinstaller
+RUN git clone --branch "v$PYINSTALLER_VERSION" --depth 1 https://github.com/pyinstaller/pyinstaller.git /tmp/pyinstaller && \
+    cd /tmp/pyinstaller/bootloader && \
+    python3 ./waf configure all && \
+    pip install ..
+
+RUN pyinstaller --version
+
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/scripts/installer.sh b/scripts/installer.sh
new file mode 100755
index 00000000..f911b510
--- /dev/null
+++ b/scripts/installer.sh
@@ -0,0 +1,310 @@
+#!/bin/sh
+
+set -e
+
+# ----------------------------------------
+# Configuration
+# ----------------------------------------
+
+WORK_DIR=$(pwd)
+TEMP_DIR="/tmp/cloudlift"
+PACKAGE_NAME="cloudlift"
+PACKAGE_DEST_DIR="/usr/local"
+PACKAGE_BIN_DIR="/usr/local/bin"
+
+PACKAGE_VERSION="latest"
+ARCH_TYPE=$(uname -m)
+OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]')
+
+if [ "${OS_TYPE}" = "linux" ]; then
+  # check if the OS is Alpine Linux
+  if [ -f /etc/os-release ]; then
+    . /etc/os-release
+    if [ "${ID}" = "alpine" ]; then
+      OS_TYPE="alpine"
+    fi
+  fi
+fi
+
+REPO_OWNER="GetSimpl"
+DOWNLOAD_BASE_URL=${CLOUDLIFT_DOWNLOAD_BASE_URL:-"https://github.com/${REPO_OWNER}/cloudlift/releases/download"}
+LATEST_RELEASE_URL=${CLOUDLIFT_LATEST_RELEASE_URL:-"https://api.github.com/repos/${REPO_OWNER}/cloudlift/releases/latest"}
+
+IS_UNINSTALL=false
+AUTO_APPROVE=false
+
+SUPPORTED_TYPES="linux-x86_64 alpine-x86_64 darwin-arm64"
+
+# ----------------------------------------
+# Helper Functions
+# ----------------------------------------
+
+print_message() {
+  # Using printf for better compatibility
+  printf "\033[0;%sm[%s]\033[0m %s\n" "$1" "$2" "$3"
+}
+
+print_info() { print_message "32" "INFO" "$1"; }
+print_warning() { print_message "33" "WARN" "$1"; }
+print_error() {
+  print_message "31" "ERROR" "$1"
+  exit 1
+}
+
+confirm_message() {
+  if [ "${AUTO_APPROVE}" = true ]; then
+    print_info "Auto-approving..."
+    return 0
+  fi
+  printf "%s [y/N]: " "$1"
+  read -r confirm
+  case "$confirm" in
+  [Yy]*) return 0 ;;
+  *)
+    print_warning "Exiting..."
+    exit 0
+    ;;
+  esac
+}
+
+# ----------------------------------------
+# Sudo Handling
+# ----------------------------------------
+
+check_sudo_access() {
+  if [ "$(id -u)" = "0" ]; then
+    print_info "Running as root, sudo access available."
+    return 0
+  fi
+  
+  print_info "Checking for sudo access (may prompt for your password)"
+  
+  if command -v sudo >/dev/null; then
+    if sudo -v 2>/dev/null; then
+      print_info "Sudo access is available."
+      return 0
+    else
+      print_warning "Sudo access is not available without a password."
+      return 1
+    fi
+  else
+    print_warning "Sudo is not installed on this system."
+    return 1
+  fi
+}
+
+exec_with_sudo() {
+  if [ "$(id -u)" = "0" ]; then
+    print_info "Executing as root: $*"
+    "$@"
+  elif check_sudo_access; then
+    print_info "Executing with sudo: $*"
+    sudo "$@"
+  else
+    print_error "This operation requires root privileges. Please run as root or ensure sudo access is available."
+  fi
+}
+
+# ----------------------------------------
+# Core Functions
+# ----------------------------------------
+
+get_latest_release() {
+  print_info "Getting latest release version..."
+  latest_release=$(curl -sL "${LATEST_RELEASE_URL}" | jq -r ".tag_name")
+  [ -z "${latest_release}" ] && print_error "Failed to get latest release version."
+  PACKAGE_VERSION=$(echo "${latest_release}" | sed 's/^v//')
+  print_info "Latest release version: v${PACKAGE_VERSION}"
+}
+
+download_cloudlift() {
+  ARCHIVE_NAME="${PACKAGE_NAME}-${OS_TYPE}-${ARCH_TYPE}-v${PACKAGE_VERSION}"
+  print_info "Downloading Cloudlift package for: ${ARCHIVE_NAME}"
+
+  supported_os_arch="${OS_TYPE}-${ARCH_TYPE}"
+  if ! echo "${SUPPORTED_TYPES}" | grep -q "${supported_os_arch}"; then
+    print_error "Unsupported OS type: ${OS_TYPE}-${ARCH_TYPE}. Cloudlift package is not available for this OS type yet."
+  fi
+
+  download_url="${DOWNLOAD_BASE_URL}/v${PACKAGE_VERSION}/${ARCHIVE_NAME}.tar.gz"
+  print_info "Downloading from: ${download_url}"
+  mkdir -p "${TEMP_DIR}"
+  if ! exec_with_sudo curl --fail -L -o "${TEMP_DIR}/${ARCHIVE_NAME}.tar.gz" "${download_url}"; then
+    print_error "Failed to download Cloudlift package."
+  fi
+}
+
+verify_checksum() {
+  print_info "Verifying checksum..."
+  checksum_url="${DOWNLOAD_BASE_URL}/v${PACKAGE_VERSION}/checksums.txt"
+  if ! exec_with_sudo curl --fail -L -o "${TEMP_DIR}/checksums.txt" "${checksum_url}"; then
+    print_error "Failed to download checksum."
+  fi
+
+  checksum=$(grep "${ARCHIVE_NAME}.tar.gz" "${TEMP_DIR}/checksums.txt" | cut -d' ' -f2)
+  if [ "${OS_TYPE}" = "darwin" ]; then
+    verify_cmd="shasum -a 256"
+  else
+    verify_cmd="sha256sum"
+  fi
+
+  if ! ${verify_cmd} "${TEMP_DIR}/${ARCHIVE_NAME}.tar.gz" | grep -q "${checksum}"; then
+    print_error "Checksum mismatch. Installation aborted for security reasons."
+  fi
+}
+
+unarchive_cloudlift() {
+  print_info "Unarchiving Cloudlift package..."
+  [ -d "${TEMP_DIR}/${ARCHIVE_NAME}" ] && exec_with_sudo rm -rf "${TEMP_DIR}/${ARCHIVE_NAME}"
+  if ! exec_with_sudo tar -xzf "${TEMP_DIR}/${ARCHIVE_NAME}.tar.gz" -C "${TEMP_DIR}" >/dev/null; then
+    print_error "Failed to unarchive Cloudlift package."
+  fi
+}
+
+install_cloudlift() {
+  print_info "Installing Cloudlift package..."
+  [ -d "${PACKAGE_DEST_DIR}/${PACKAGE_NAME}" ] && exec_with_sudo rm -rf "${PACKAGE_DEST_DIR}/${PACKAGE_NAME}"
+  [ -L "${PACKAGE_BIN_DIR}/${PACKAGE_NAME}" ] && exec_with_sudo rm "${PACKAGE_BIN_DIR}/${PACKAGE_NAME}"
+
+  if ! exec_with_sudo cp -r "${TEMP_DIR}/${ARCHIVE_NAME}" "${PACKAGE_DEST_DIR}/${PACKAGE_NAME}"; then
+    print_error "Failed to copy Cloudlift package."
+  fi
+  if ! exec_with_sudo ln -s "${PACKAGE_DEST_DIR}/${PACKAGE_NAME}/cloudlift" "${PACKAGE_BIN_DIR}/${PACKAGE_NAME}"; then
+    print_error "Failed to create symlink."
+  fi
+}
+
+test_installation() {
+  print_info "Testing Cloudlift installation..."
+  if ! command -v "cloudlift" >/dev/null; then
+    print_error "Failed to install Cloudlift package."
+  fi
+  if ! cloudlift --help >/dev/null; then
+    print_error "Failed to run Cloudlift package."
+  fi
+  installed_version=$(cloudlift --version | cut -d' ' -f3)
+  if [ "${installed_version}" != "${PACKAGE_VERSION}" ]; then
+    print_error "Cloudlift package version mismatch."
+  fi
+  print_info "Cloudlift package installation test passed."
+}
+
+uninstall_cloudlift() {
+  print_info "Uninstalling Cloudlift package..."
+  if ! command -v "cloudlift" >/dev/null; then
+    print_info "Cloudlift package not found. Exiting..."
+    exit 0
+  fi
+  current_version=$(cloudlift --version | cut -d' ' -f3)
+  print_info "Current Cloudlift package version: ${current_version}"
+  confirm_message "Do you want to uninstall Cloudlift package?"
+
+  [ -d "${PACKAGE_DEST_DIR}/${PACKAGE_NAME}" ] && exec_with_sudo rm -rf "${PACKAGE_DEST_DIR}/${PACKAGE_NAME}"
+  [ -L "${PACKAGE_BIN_DIR}/${PACKAGE_NAME}" ] &&  exec_with_sudo rm "${PACKAGE_BIN_DIR}/${PACKAGE_NAME}"
+
+  # clear the shell cache
+  hash -r
+  if command -v "cloudlift"; then
+    print_error "Failed to uninstall Cloudlift package."
+  fi
+  print_info "Cloudlift package uninstalled successfully."
+}
+
+preflight_check() {
+  print_info "Running preflight checks..."
+  [ -z "${TERM}" ] && AUTO_APPROVE=true
+  for cmd in curl tar gzip jq; do
+    if ! command -v "$cmd" >/dev/null; then
+      print_error "$cmd is required."
+    fi
+  done
+
+  [ "${PACKAGE_VERSION}" = "latest" ] && get_latest_release
+
+  if command -v "cloudlift" >/dev/null; then
+    current_version=$(cloudlift --version | cut -d' ' -f3)
+    [ -z "${current_version}" ] && print_warning "The current Cloudlift did not install properly or is corrupted."
+    print_info "Cloudlift package already installed; version: v${current_version}"
+    confirm_message "Do you want to reinstall Cloudlift with version v${PACKAGE_VERSION}?"
+  fi
+}
+
+# ----------------------------------------
+# Main Execution
+# ----------------------------------------
+
+usage() {
+  echo "
+  Usage: $0 [options]
+
+  Options:
+    -h, --help          Display this help message
+    -v, --version       Install specific version of Cloudlift package
+    -u, --uninstall     Uninstall Cloudlift package
+    -y, --auto-approve  Auto-approve installation
+  "
+}
+
+parse_arguments() {
+  while [ $# -gt 0 ]; do
+    case "$1" in
+    -h | --help)
+      usage
+      exit 0
+      ;;
+    -v | --version)
+      shift
+      PACKAGE_VERSION=$(echo "$1" | sed 's/^v//')
+      ;;
+    -u | --uninstall)
+      IS_UNINSTALL=true
+      ;;
+    -y | --auto-approve)
+      AUTO_APPROVE=true
+      ;;
+    *)
+      print_error "Invalid argument: $1"
+      ;;
+    esac
+    shift
+  done
+}
+
+main() {
+  parse_arguments "$@"
+
+  print_info "Cloudlift installation started..."
+  print_info "For OS: ${OS_TYPE}, Arch: ${ARCH_TYPE}, Version: ${PACKAGE_VERSION}"
+
+  if [ "${IS_UNINSTALL}" = true ]; then
+    uninstall_cloudlift
+  else
+    preflight_check
+    download_cloudlift
+    verify_checksum
+    unarchive_cloudlift
+    install_cloudlift
+    test_installation
+  fi
+}
+
+status_message() {
+  if [ $1 -eq 0 ]; then
+    version=$(cloudlift --version | cut -d' ' -f3)
+    printf '
+            _                    _  _  _   __  _
+       ___ | |  ___   _   _   __| || |(_) / _|| |_
+      / __|| | / _ \ | | | | / _` || || || |_ | __|
+     | (__ | || (_) || |_| || (_| || || ||  _|| |_
+      \___||_| \___/  \__,_| \__,_||_||_||_|   \__|
+
+        \n'
+    print_info "Cloudlift package installed successfully; version: ${version}"
+    print_info "Run 'cloudlift --help' to know more."
+  else
+    print_error "Failed to install Cloudlift package."
+  fi
+}
+
+main "$@"
+status_message $?
\ No newline at end of file


Test patch:
diff --git a/scripts/test_installer.sh b/scripts/test_installer.sh
new file mode 100644
index 00000000..e8e58418
--- /dev/null
+++ b/scripts/test_installer.sh
@@ -0,0 +1,136 @@
+#!/bin/bash
+
+set -euo pipefail
+
+cleanup() {
+    echo "Cleaning up..."
+    kill $SERVER_PID 2>/dev/null || true
+}
+
+current_tag="$1"
+if [ -z "$current_tag" ]; then
+    echo "Error: No tag provided" >&2
+    exit 1
+fi
+echo "Current tag: $current_tag"
+
+# Get the current tag
+# current_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0")
+
+create_mock_files() {
+    # Create JSON response to mock GitHub's release API endpoint
+    echo "{\"tag_name\":\"$current_tag\"}" > release_api_res.json
+}
+
+start_server() {
+    python3 -m http.server 3000 &
+    SERVER_PID=$!
+    echo "Server started with PID: $SERVER_PID"
+
+    # Wait for the server to start
+    while ! curl -s http://localhost:3000 > /dev/null; do
+        sleep 1
+    done
+}
+
+run_installer() {
+    # Run installer with given arguments
+    local input="$1"
+    shift
+    echo "$input" | sudo -E ./installer.sh "$@" 2>&1
+}
+
+test_fresh_install() {
+    echo "Testing fresh installation..."
+    run_installer "y"
+    current_tag_without_v="${current_tag#v}" 
+    cloudlift --version
+    if cloudlift --version | grep -q $current_tag_without_v; then
+        echo "Fresh install test passed"
+    else
+        echo "Fresh install test failed"
+        return 1
+    fi
+}
+
+test_reinstall() {
+    echo "Testing reinstallation..."
+    run_installer "y"
+    current_tag_without_v="${current_tag#v}" 
+    if sudo cloudlift --version | grep -q $current_tag_without_v; then
+        echo "Reinstall test passed"
+    else
+        echo "Reinstall test failed"
+        return 1
+    fi
+}
+
+test_specific_version() {
+    echo "Testing installation of specific version..."
+    current_tag_without_v="${current_tag#v}"
+    run_installer "y" "-v" $current_tag_without_v
+    if cloudlift --version | grep -q $current_tag_without_v; then
+        echo "Specific version install test passed"
+    else
+        echo "Specific version install test failed"
+        return 1
+    fi
+}
+
+test_uninstall() {
+    echo "Testing uninstallation..."
+    output=$(run_installer "y" "-u" 2>&1)
+    # echo "$output" 
+    if echo "$output" | grep -q "Cloudlift package uninstalled successfully"; then
+        if which cloudlift &> /dev/null; then
+            echo "Uninstall test failed: cloudlift command still exists"
+            return 1
+        else
+            echo "Uninstall test passed"
+        fi
+    else
+        echo "Uninstall test failed: uninstallation message not found"
+        return 1
+    fi
+}
+
+test_auto_approve() {
+    echo "Testing auto-approve installation..."
+    output=$(sudo -E ./installer.sh -y 2>&1)
+    echo "Installer output:"
+    echo "$output"
+    
+    if echo "$output" | grep -q "Cloudlift package installed successfully"; then
+        installed_version=$(sudo cloudlift --version | cut -d' ' -f3)
+        echo "Installed Cloudlift version: $installed_version"
+        echo "Current tag: $current_tag"
+        
+        if [ -n "$installed_version" ]; then
+            echo "Auto-approve install test passed"
+        else
+            echo "Auto-approve install test failed: Could not detect installed version"
+            return 1
+        fi
+    else
+        echo "Auto-approve install test failed: Installation was not successful"
+        return 1
+    fi
+}
+
+main() {
+    create_mock_files
+    start_server
+
+    export CLOUDLIFT_LATEST_RELEASE_URL="http://localhost:3000/release_api_res.json"
+    export CLOUDLIFT_DOWNLOAD_BASE_URL="http://localhost:3000"
+
+    test_fresh_install
+    test_reinstall
+    test_specific_version
+    test_uninstall
+    test_auto_approve
+
+    echo "All tests completed."
+}
+
+main
\ No newline at end of file