diff --git a/README.md b/README.md index 71a2903..b345d57 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ # NDArray Library (Java) -![CI](https://github.com/sMouaad/DevOps-Project/actions/workflows/ci.yml/badge.svg) -![Docker](https://github.com/sMouaad/DevOps-Project/actions/workflows/docker.yml/badge.svg) +[![CI](https://github.com/sMouaad/DevOps-Project/actions/workflows/ci.yml/badge.svg)](https://github.com/sMouaad/DevOps-Project/actions/workflows/ci.yml) +[![Docker](https://github.com/sMouaad/DevOps-Project/actions/workflows/docker.yml/badge.svg)](https://github.com/sMouaad/DevOps-Project/actions/workflows/docker.yml) +[![Pages](https://github.com/sMouaad/DevOps-Project/actions/workflows/deploy-site.yml/badge.svg)](https://github.com/sMouaad/DevOps-Project/actions/workflows/deploy-site.yml) +[![Publish SNAPSHOT](https://github.com/sMouaad/DevOps-Project/actions/workflows/publish-snapshot.yml/badge.svg)](https://github.com/sMouaad/DevOps-Project/actions/workflows/publish-snapshot.yml) +[![Docs](https://img.shields.io/badge/docs-live-success)](https://smouaad.github.io/DevOps-Project/) +[![Java 17](https://img.shields.io/badge/java-17-blue)](https://adoptium.net/) +[![Maven](https://img.shields.io/badge/build-maven-C71A36)](https://maven.apache.org/) Small NumPy-inspired Java library for ndarray operations, developed as a DevOps team project. @@ -24,6 +29,7 @@ Current snapshot date: 2026-04-04. - 2D ndarray support with matrix validation. - Creation helpers: `array`, `zeros`, `arange`. - Addition operations: `add` and `addInPlace` with strict shape checks. +- Optional extensions: scalar addition and `sum()` reduction. - `reshape(int... shape)` with size consistency validation. - NumPy-like string display for 1D arrays (including ellipsis for large arrays). - Defensive copy behavior on constructors and exports. @@ -38,6 +44,8 @@ NdArray c = NdArray.arange(0f, 6f, 2f); NdArray m = NdArray.array(new float[][] {{1f, 2f}, {3f, 4f}}); NdArray sum = m.add(NdArray.array(new float[][] {{10f, 10f}, {10f, 10f}})); NdArray reshaped = NdArray.arange(6f).reshape(2, 3); +NdArray shifted = a.add(10f); +float total = shifted.sum(); ``` ## Tech Stack and Tooling Choices @@ -156,6 +164,19 @@ The Docker workflow (`.github/workflows/docker.yml`): - Uses multi-stage build for minimal image size - Caches layers with GitHub Actions cache +## Cloud Deployment + +We use Terraform + Ansible to deploy the container to GCP. Spins up a free-tier e2-micro VM with Docker. + +```bash +cd infrastructure/scripts +./setup-gcp.sh # one-time setup +./deploy.sh # creates VM and deploys container +./destroy.sh # tear down when done +``` + +See [infrastructure/README.md](infrastructure/README.md) for details. + ## Current Status vs Mandatory Scope Implemented: @@ -163,7 +184,10 @@ Implemented: - 1D and 2D ndarray support. - Creation functions (`array`, `zeros`, `arange`). - Addition operations (`add`, `addInPlace`). +- Scalar addition and `sum()` reduction. - Reshape with validation. +- Docker containerization with CI/CD pipeline. +- Infrastructure as Code for GCP deployment. In progress / next: diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore new file mode 100644 index 0000000..218909d --- /dev/null +++ b/infrastructure/.gitignore @@ -0,0 +1,29 @@ +*.tfstate +*.tfstate.* +*.tfvars +!terraform.tfvars.example +.terraform/ +.terraform.lock.hcl +tfplan +*.tfplan +crash.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +*.pem +*.pub +id_rsa* +ssh_key* + +credentials.json +*-credentials.json +*-key.json +service-account*.json + +*.retry +ansible/inventory/hosts.yml + +.env +.env.* diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000..844ca6b --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,122 @@ +# IaC - GCP Deployment + +Terraform + Ansible setup to deploy our ndarray container on GCP. + +## What we're deploying + +- 1 VM (e2-micro, free tier) on europe-west1 +- Custom VPC with firewall for SSH/HTTP +- Docker container running the ndarray demo + +## Setup + +You need Terraform, Ansible, and gcloud CLI installed. Check with: +```bash +terraform --version && ansible --version && gcloud --version +``` + +### GCP account + +Create a project on https://console.cloud.google.com and grab the project ID. + +```bash +gcloud auth login +gcloud config set project YOUR_PROJECT_ID +``` + +## Deploy + +```bash +cd infrastructure/scripts + +# first time only - enables APIs, creates service account, generates keys +./setup-gcp.sh + +# set creds +export GOOGLE_APPLICATION_CREDENTIALS="$(pwd)/../terraform/credentials.json" + +# deploy everything +./deploy.sh +``` + +The deploy script runs terraform to create the VM, then ansible to install docker and start the container. + +### Check it worked + +```bash +ssh -i ../ssh_key ansible@ +docker ps +docker logs ndarray-demo +``` + +## Manual way + +If you want more control: + +```bash +# terraform +cd infrastructure/terraform +terraform init +terraform plan +terraform apply +terraform output vm_external_ip + +# ansible +cd ../ansible +ansible-playbook playbooks/setup.yml +ansible-playbook playbooks/deploy.yml +``` + +## Tear down + +Don't forget to destroy when you're done: +```bash +./destroy.sh +# or: terraform destroy +``` + +## Files + +``` +infrastructure/ +├── terraform/ # VM, VPC, firewall definitions +├── ansible/ # playbooks + roles for docker/app setup +└── scripts/ # setup, deploy, destroy helpers +``` + +## Config + +Edit `terraform/terraform.tfvars`: +```hcl +project_id = "your-project-id" +project_name = "ndarray" +region = "europe-west1" +zone = "europe-west1-b" +machine_type = "e2-micro" +ssh_user = "ansible" +``` + +## Common issues + +**Can't SSH?** +```bash +gcloud compute instances list # check VM is running +ssh -i infrastructure/ssh_key -o StrictHostKeyChecking=no ansible@ +``` + +**Terraform state messed up?** +```bash +terraform refresh +terraform taint google_compute_instance.vm +terraform apply +``` + +**Container not starting?** +```bash +sudo docker ps -a +sudo docker logs ndarray-demo +``` + +## Costs + +e2-micro is free tier (1/month). Static IP has a small cost when unattached. Just run destroy.sh when not using it. diff --git a/infrastructure/ansible/ansible.cfg b/infrastructure/ansible/ansible.cfg new file mode 100644 index 0000000..d0b9b28 --- /dev/null +++ b/infrastructure/ansible/ansible.cfg @@ -0,0 +1,18 @@ +[defaults] +inventory = inventory/hosts.yml +roles_path = roles +remote_user = ansible +private_key_file = ../ssh_key +host_key_checking = False +retry_files_enabled = False +interpreter_python = auto_silent + +[privilege_escalation] +become = True +become_method = sudo +become_user = root +become_ask_pass = False + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no +pipelining = True diff --git a/infrastructure/ansible/inventory/hosts.yml.tpl b/infrastructure/ansible/inventory/hosts.yml.tpl new file mode 100644 index 0000000..a6d9c3f --- /dev/null +++ b/infrastructure/ansible/inventory/hosts.yml.tpl @@ -0,0 +1,9 @@ +--- +all: + children: + app_servers: + hosts: + ndarray-vm: + ansible_host: "{{ vm_ip }}" + ansible_user: ansible + ansible_ssh_private_key_file: "../ssh_key" diff --git a/infrastructure/ansible/playbooks/deploy.yml b/infrastructure/ansible/playbooks/deploy.yml new file mode 100644 index 0000000..b8d5288 --- /dev/null +++ b/infrastructure/ansible/playbooks/deploy.yml @@ -0,0 +1,12 @@ +--- +# Deploy playbook - Builds and runs the application container +- name: Deploy Application + hosts: app_servers + become: yes + + vars: + app_name: ndarray-demo + app_version: latest + + roles: + - app diff --git a/infrastructure/ansible/playbooks/setup.yml b/infrastructure/ansible/playbooks/setup.yml new file mode 100644 index 0000000..efa71d4 --- /dev/null +++ b/infrastructure/ansible/playbooks/setup.yml @@ -0,0 +1,16 @@ +--- +# Setup playbook - Configures VM with Docker +- name: Configure VM + hosts: app_servers + become: yes + + tasks: + - name: Wait for system to be ready + wait_for_connection: + timeout: 300 + + - name: Gather facts + setup: + + roles: + - docker diff --git a/infrastructure/ansible/roles/app/defaults/main.yml b/infrastructure/ansible/roles/app/defaults/main.yml new file mode 100644 index 0000000..308e2fe --- /dev/null +++ b/infrastructure/ansible/roles/app/defaults/main.yml @@ -0,0 +1,4 @@ +--- +app_name: ndarray-demo +app_version: latest +container_ports: [] diff --git a/infrastructure/ansible/roles/app/tasks/main.yml b/infrastructure/ansible/roles/app/tasks/main.yml new file mode 100644 index 0000000..aee82cb --- /dev/null +++ b/infrastructure/ansible/roles/app/tasks/main.yml @@ -0,0 +1,55 @@ +--- +- name: Create app directory + file: + path: /opt/ndarray + state: directory + mode: '0755' + +- name: Copy Dockerfile + copy: + src: "{{ playbook_dir }}/../../../Dockerfile" + dest: /opt/ndarray/Dockerfile + mode: '0644' + +- name: Copy pom.xml + copy: + src: "{{ playbook_dir }}/../../../pom.xml" + dest: /opt/ndarray/pom.xml + mode: '0644' + +- name: Copy source code + copy: + src: "{{ playbook_dir }}/../../../src/" + dest: /opt/ndarray/src/ + mode: '0644' + directory_mode: '0755' + +- name: Build Docker image + community.docker.docker_image: + name: "{{ app_name }}" + tag: "{{ app_version }}" + source: build + build: + path: /opt/ndarray + pull: yes + state: present + register: docker_build + +- name: Remove existing container if present + community.docker.docker_container: + name: "{{ app_name }}" + state: absent + ignore_errors: yes + +- name: Run application container + community.docker.docker_container: + name: "{{ app_name }}" + image: "{{ app_name }}:{{ app_version }}" + state: started + restart_policy: unless-stopped + ports: "{{ container_ports | default([]) }}" + register: container_result + +- name: Display container status + debug: + msg: "Container '{{ app_name }}' is running with ID: {{ container_result.container.Id[:12] }}" diff --git a/infrastructure/ansible/roles/docker/tasks/main.yml b/infrastructure/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000..401f6e2 --- /dev/null +++ b/infrastructure/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: Install Docker dependencies + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - python3-pip + state: present + update_cache: yes + +- name: Add Docker GPG key + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + +- name: Install Docker + apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + state: present + update_cache: yes + +- name: Start and enable Docker service + systemd: + name: docker + state: started + enabled: yes + +- name: Add user to docker group + user: + name: "{{ ansible_user }}" + groups: docker + append: yes + +- name: Install Docker Python SDK + pip: + name: docker + state: present diff --git a/infrastructure/scripts/deploy.sh b/infrastructure/scripts/deploy.sh new file mode 100755 index 0000000..c856965 --- /dev/null +++ b/infrastructure/scripts/deploy.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# Deploy Script - Provisions infrastructure and deploys application +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${GREEN}=== NDArray Infrastructure Deployment ===${NC}" + +# Check prerequisites +check_prerequisites() { + echo -e "${YELLOW}Checking prerequisites...${NC}" + + if ! command -v terraform &> /dev/null; then + echo -e "${RED}Error: Terraform is not installed${NC}" + echo "Install from: https://developer.hashicorp.com/terraform/install" + exit 1 + fi + + if ! command -v ansible &> /dev/null; then + echo -e "${RED}Error: Ansible is not installed${NC}" + echo "Install with: pip install ansible" + exit 1 + fi + + if [ ! -f "$ROOT_DIR/terraform/terraform.tfvars" ]; then + echo -e "${RED}Error: terraform.tfvars not found${NC}" + echo "Run ./setup-gcp.sh first" + exit 1 + fi + + if [ ! -f "$ROOT_DIR/ssh_key" ]; then + echo -e "${RED}Error: SSH key not found${NC}" + echo "Run ./setup-gcp.sh first" + exit 1 + fi + + echo -e "${GREEN}Prerequisites OK${NC}" +} + +# Terraform apply +provision_infrastructure() { + echo -e "${YELLOW}Provisioning infrastructure with Terraform...${NC}" + + cd "$ROOT_DIR/terraform" + + # Initialize Terraform + terraform init + + # Plan and apply + terraform plan -out=tfplan + terraform apply tfplan + + # Get outputs + VM_IP=$(terraform output -raw vm_external_ip) + echo -e "${GREEN}VM provisioned with IP: $VM_IP${NC}" + + # Export for Ansible + export VM_IP + cd "$SCRIPT_DIR" +} + +# Generate Ansible inventory +generate_inventory() { + echo -e "${YELLOW}Generating Ansible inventory...${NC}" + + VM_IP=$(cd "$ROOT_DIR/terraform" && terraform output -raw vm_external_ip) + + cat > "$ROOT_DIR/ansible/inventory/hosts.yml" << EOF +--- +all: + children: + app_servers: + hosts: + ndarray-vm: + ansible_host: ${VM_IP} + ansible_user: ansible + ansible_ssh_private_key_file: "../ssh_key" +EOF + + echo -e "${GREEN}Inventory generated${NC}" +} + +# Wait for VM to be ready +wait_for_vm() { + echo -e "${YELLOW}Waiting for VM to be ready...${NC}" + + VM_IP=$(cd "$ROOT_DIR/terraform" && terraform output -raw vm_external_ip) + + for i in {1..30}; do + if ssh -i "$ROOT_DIR/ssh_key" -o StrictHostKeyChecking=no -o ConnectTimeout=5 ansible@"$VM_IP" "echo 'VM ready'" 2>/dev/null; then + echo -e "${GREEN}VM is ready${NC}" + return 0 + fi + echo "Waiting for SSH... ($i/30)" + sleep 10 + done + + echo -e "${RED}Timeout waiting for VM${NC}" + exit 1 +} + +# Run Ansible playbooks +configure_and_deploy() { + echo -e "${YELLOW}Configuring VM with Ansible...${NC}" + + cd "$ROOT_DIR/ansible" + + # Install required Ansible collections + ansible-galaxy collection install community.docker --force + + # Run setup playbook (install Docker) + echo -e "${YELLOW}Installing Docker...${NC}" + ansible-playbook playbooks/setup.yml + + # Run deploy playbook (deploy app) + echo -e "${YELLOW}Deploying application...${NC}" + ansible-playbook playbooks/deploy.yml + + cd "$SCRIPT_DIR" +} + +# Main execution +main() { + check_prerequisites + provision_infrastructure + generate_inventory + wait_for_vm + configure_and_deploy + + # Final output + VM_IP=$(cd "$ROOT_DIR/terraform" && terraform output -raw vm_external_ip) + + echo "" + echo -e "${GREEN}=== Deployment Complete ===${NC}" + echo "" + echo "VM External IP: $VM_IP" + echo "" + echo "Connect to VM:" + echo " ssh -i $ROOT_DIR/ssh_key ansible@$VM_IP" + echo "" + echo "Check container logs:" + echo " ssh -i $ROOT_DIR/ssh_key ansible@$VM_IP 'docker logs ndarray-demo'" + echo "" + echo "To destroy infrastructure:" + echo " ./destroy.sh" +} + +main "$@" diff --git a/infrastructure/scripts/destroy.sh b/infrastructure/scripts/destroy.sh new file mode 100755 index 0000000..3e710ee --- /dev/null +++ b/infrastructure/scripts/destroy.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Destroy Script - Removes all provisioned infrastructure +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +echo -e "${RED}=== Infrastructure Destruction ===${NC}" +echo "" +echo -e "${YELLOW}WARNING: This will destroy all provisioned resources!${NC}" +echo "This action cannot be undone." +echo "" + +read -p "Are you sure you want to destroy all infrastructure? (yes/no): " confirm + +if [ "$confirm" != "yes" ]; then + echo "Destruction cancelled." + exit 0 +fi + +cd "$ROOT_DIR/terraform" + +# Show what will be destroyed +echo -e "${YELLOW}Resources to be destroyed:${NC}" +terraform plan -destroy + +echo "" +read -p "Proceed with destruction? (yes/no): " final_confirm + +if [ "$final_confirm" != "yes" ]; then + echo "Destruction cancelled." + exit 0 +fi + +# Destroy infrastructure +echo -e "${YELLOW}Destroying infrastructure...${NC}" +terraform destroy -auto-approve + +# Clean up local files +echo -e "${YELLOW}Cleaning up local files...${NC}" +rm -f "$ROOT_DIR/ansible/inventory/hosts.yml" +rm -f "$ROOT_DIR/terraform/tfplan" + +echo "" +echo -e "${GREEN}=== Destruction Complete ===${NC}" +echo "" +echo "All cloud resources have been removed." +echo "Your student credits are preserved for future use." +echo "" +echo "Files preserved (for future deployments):" +echo " - terraform.tfvars" +echo " - SSH keys" +echo " - credentials.json" diff --git a/infrastructure/scripts/setup-gcp.sh b/infrastructure/scripts/setup-gcp.sh new file mode 100755 index 0000000..d6e897f --- /dev/null +++ b/infrastructure/scripts/setup-gcp.sh @@ -0,0 +1,128 @@ +#!/bin/bash +# Run this once to configure GCP for Terraform +set -e + +# colors makes it very cool, looks clean and professional +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== GCP Setup for Terraform ===${NC}" + +# Check if gcloud is installed +if ! command -v gcloud &> /dev/null; then + echo -e "${RED}Error: gcloud CLI is not installed${NC}" + echo "Install from: https://cloud.google.com/sdk/docs/install" + exit 1 +fi + +# Check if logged in +ACCOUNT=$(gcloud config get-value account 2>/dev/null) +if [ -z "$ACCOUNT" ]; then + echo -e "${YELLOW}Not logged in. Running gcloud auth login...${NC}" + gcloud auth login +fi + +echo -e "${GREEN}Logged in as: $ACCOUNT${NC}" + +# Get or set project +PROJECT_ID=$(gcloud config get-value project 2>/dev/null) +if [ -z "$PROJECT_ID" ]; then + echo -e "${YELLOW}No project set. Available projects:${NC}" + gcloud projects list + echo "" + read -p "Enter your GCP Project ID: " PROJECT_ID + gcloud config set project "$PROJECT_ID" +fi + +echo -e "${GREEN}Using project: $PROJECT_ID${NC}" + +# Enable required APIs +echo -e "${YELLOW}Enabling required GCP APIs...${NC}" +gcloud services enable compute.googleapis.com +gcloud services enable cloudresourcemanager.googleapis.com +gcloud services enable iam.googleapis.com + +echo -e "${GREEN}APIs enabled successfully${NC}" + +# Create service account for Terraform +SA_NAME="terraform-sa" +SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" + +echo -e "${YELLOW}Creating service account: $SA_NAME${NC}" +if ! gcloud iam service-accounts describe "$SA_EMAIL" &>/dev/null; then + gcloud iam service-accounts create "$SA_NAME" \ + --display-name="Terraform Service Account" \ + --description="Service account for Terraform infrastructure management" + echo -e "${GREEN}Service account created${NC}" +else + echo -e "${GREEN}Service account already exists${NC}" +fi + +# Grant roles to service account +echo -e "${YELLOW}Granting IAM roles to service account...${NC}" +ROLES=( + "roles/compute.admin" + "roles/iam.serviceAccountUser" + "roles/resourcemanager.projectIamAdmin" +) + +for role in "${ROLES[@]}"; do + gcloud projects add-iam-policy-binding "$PROJECT_ID" \ + --member="serviceAccount:$SA_EMAIL" \ + --role="$role" \ + --quiet +done + +echo -e "${GREEN}IAM roles granted${NC}" + +# Generate and download service account key +KEY_FILE="../terraform/credentials.json" +echo -e "${YELLOW}Generating service account key...${NC}" +gcloud iam service-accounts keys create "$KEY_FILE" \ + --iam-account="$SA_EMAIL" 2>/dev/null || true + +if [ -f "$KEY_FILE" ]; then + echo -e "${GREEN}Service account key saved to: $KEY_FILE${NC}" +else + echo -e "${YELLOW}Key file already exists or could not be created${NC}" +fi + +# Generate SSH key pair for VM access +SSH_KEY="../ssh_key" +if [ ! -f "$SSH_KEY" ]; then + echo -e "${YELLOW}Generating SSH key pair...${NC}" + ssh-keygen -t rsa -b 4096 -f "$SSH_KEY" -N "" -C "ansible@ndarray-vm" + echo -e "${GREEN}SSH key pair generated${NC}" +else + echo -e "${GREEN}SSH key already exists${NC}" +fi + +# Create terraform.tfvars +TFVARS="../terraform/terraform.tfvars" +if [ ! -f "$TFVARS" ]; then + echo -e "${YELLOW}Creating terraform.tfvars...${NC}" + cat > "$TFVARS" << EOF +project_id = "$PROJECT_ID" +project_name = "ndarray" +region = "europe-west1" +zone = "europe-west1-b" +machine_type = "e2-micro" +ssh_user = "ansible" +EOF + echo -e "${GREEN}terraform.tfvars created${NC}" +else + echo -e "${GREEN}terraform.tfvars already exists${NC}" +fi + +# Set environment variable for Terraform +echo "" +echo -e "${GREEN}=== Setup Complete ===${NC}" +echo "" +echo "Next steps:" +echo " 1. Set credentials environment variable:" +echo " export GOOGLE_APPLICATION_CREDENTIALS=\"\$(pwd)/../terraform/credentials.json\"" +echo "" +echo " 2. Run the deploy script:" +echo " ./deploy.sh" diff --git a/infrastructure/terraform/main.tf b/infrastructure/terraform/main.tf new file mode 100644 index 0000000..f65625e --- /dev/null +++ b/infrastructure/terraform/main.tf @@ -0,0 +1,117 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + google = { + source = "hashicorp/google" + version = "~> 5.0" + } + } +} + +provider "google" { + project = var.project_id + region = var.region + zone = var.zone +} + +# VPC Network +resource "google_compute_network" "vpc" { + name = "${var.project_name}-vpc" + auto_create_subnetworks = false +} + +# Subnet +resource "google_compute_subnetwork" "subnet" { + name = "${var.project_name}-subnet" + ip_cidr_range = "10.0.1.0/24" + region = var.region + network = google_compute_network.vpc.id +} + +# Firewall - Allow SSH +resource "google_compute_firewall" "allow_ssh" { + name = "${var.project_name}-allow-ssh" + network = google_compute_network.vpc.name + + allow { + protocol = "tcp" + ports = ["22"] + } + + source_ranges = ["0.0.0.0/0"] + target_tags = ["ssh-enabled"] +} + +# Firewall - Allow HTTP/HTTPS +resource "google_compute_firewall" "allow_http" { + name = "${var.project_name}-allow-http" + network = google_compute_network.vpc.name + + allow { + protocol = "tcp" + ports = ["80", "443", "8080"] + } + + source_ranges = ["0.0.0.0/0"] + target_tags = ["http-enabled"] +} + +# Firewall - Allow ICMP (ping) +resource "google_compute_firewall" "allow_icmp" { + name = "${var.project_name}-allow-icmp" + network = google_compute_network.vpc.name + + allow { + protocol = "icmp" + } + + source_ranges = ["0.0.0.0/0"] +} + +# Static External IP +resource "google_compute_address" "static_ip" { + name = "${var.project_name}-ip" + region = var.region +} + +# Compute Instance (VM) +resource "google_compute_instance" "vm" { + name = "${var.project_name}-vm" + machine_type = var.machine_type + zone = var.zone + + tags = ["ssh-enabled", "http-enabled"] + + boot_disk { + initialize_params { + image = "ubuntu-os-cloud/ubuntu-2204-lts" + size = 20 + type = "pd-standard" + } + } + + network_interface { + subnetwork = google_compute_subnetwork.subnet.id + + access_config { + nat_ip = google_compute_address.static_ip.address + } + } + + metadata = { + ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key_path)}" + } + + metadata_startup_script = <<-EOF + #!/bin/bash + apt-get update + apt-get install -y python3 python3-pip + EOF + + labels = { + environment = "dev" + project = var.project_name + managed_by = "terraform" + } +} diff --git a/infrastructure/terraform/outputs.tf b/infrastructure/terraform/outputs.tf new file mode 100644 index 0000000..0e0ff9d --- /dev/null +++ b/infrastructure/terraform/outputs.tf @@ -0,0 +1,24 @@ +output "vm_external_ip" { + description = "External IP address of the VM" + value = google_compute_address.static_ip.address +} + +output "vm_name" { + description = "Name of the created VM" + value = google_compute_instance.vm.name +} + +output "vm_zone" { + description = "Zone where VM is deployed" + value = google_compute_instance.vm.zone +} + +output "ssh_command" { + description = "SSH command to connect to the VM" + value = "ssh -i ../ssh_key ${var.ssh_user}@${google_compute_address.static_ip.address}" +} + +output "project_id" { + description = "GCP Project ID" + value = var.project_id +} diff --git a/infrastructure/terraform/terraform.tfvars.example b/infrastructure/terraform/terraform.tfvars.example new file mode 100644 index 0000000..6c0f5b8 --- /dev/null +++ b/infrastructure/terraform/terraform.tfvars.example @@ -0,0 +1,8 @@ +# GCP Project ID (REQUIRED - get from GCP Console) +project_id = "your-gcp-project-id" + +project_name = "ndarray" +region = "europe-west1" +zone = "europe-west1-b" +machine_type = "e2-micro" +ssh_user = "ansible" diff --git a/infrastructure/terraform/variables.tf b/infrastructure/terraform/variables.tf new file mode 100644 index 0000000..4d0cac6 --- /dev/null +++ b/infrastructure/terraform/variables.tf @@ -0,0 +1,40 @@ +variable "project_id" { + description = "GCP Project ID" + type = string +} + +variable "project_name" { + description = "Name prefix for all resources" + type = string + default = "ndarray" +} + +variable "region" { + description = "GCP Region" + type = string + default = "europe-west1" +} + +variable "zone" { + description = "GCP Zone" + type = string + default = "europe-west1-b" +} + +variable "machine_type" { + description = "GCP machine type for the VM" + type = string + default = "e2-micro" # Free tier eligible +} + +variable "ssh_user" { + description = "SSH username for VM access" + type = string + default = "ansible" +} + +variable "ssh_public_key_path" { + description = "Path to SSH public key file" + type = string + default = "../ssh_key.pub" +} diff --git a/src/main/java/org/sadisamir/ndarray/NdArray.java b/src/main/java/org/sadisamir/ndarray/NdArray.java index 7e5812e..12fad92 100644 --- a/src/main/java/org/sadisamir/ndarray/NdArray.java +++ b/src/main/java/org/sadisamir/ndarray/NdArray.java @@ -263,6 +263,18 @@ public NdArray add(NdArray other) { return new NdArray(result, numDimensions, shape); } + /** + * Returns a new array with a scalar added to every element. + */ + public NdArray add(float scalar) { + float[] result = new float[totalElements]; + for (int index = 0; index < totalElements; index++) { + result[index] = flatData[index] + scalar; + } + + return new NdArray(result, numDimensions, shape); + } + /** * Adds another array to this one in place. * @@ -278,6 +290,26 @@ public void addInPlace(NdArray other) { } } + /** + * Adds a scalar to this array in place. + */ + public void addInPlace(float scalar) { + for (int index = 0; index < totalElements; index++) { + flatData[index] += scalar; + } + } + + /** + * Returns the sum of all elements in the array. + */ + public float sum() { + double total = 0.0d; + for (float value : flatData) { + total += value; + } + return (float) total; + } + /** * Returns a reshaped view of this array. * diff --git a/src/main/java/org/sadisamir/ndarray/demo/NdArrayDemo.java b/src/main/java/org/sadisamir/ndarray/demo/NdArrayDemo.java index 0f347cc..1a80d53 100644 --- a/src/main/java/org/sadisamir/ndarray/demo/NdArrayDemo.java +++ b/src/main/java/org/sadisamir/ndarray/demo/NdArrayDemo.java @@ -21,6 +21,7 @@ public static void main(String[] args) { demo1DArrayCreation(); demo2DArrayCreation(); demoArithmeticOperations(); + demoOptionalFeatures(); demoReshape(); demoLargeArrayDisplay(); printFooter(); @@ -105,6 +106,21 @@ private static void demoArithmeticOperations() { LOGGER.info(""); } + private static void demoOptionalFeatures() { + printSection("Optional Features"); + + NdArray base = NdArray.array(new float[]{1f, 2f, 3f}); + LOGGER.info("Scalar addition: base.add(10f)"); + LOGGER.info(" Base: {}", base); + NdArray shifted = base.add(10f); + LOGGER.info(" Shifted: {}", shifted); + LOGGER.info(""); + + LOGGER.info("Reduction: shifted.sum()"); + LOGGER.info(" Total: {}", shifted.sum()); + LOGGER.info(""); + } + private static void demoReshape() { printSection("Reshape Operations"); diff --git a/src/test/java/org/sadisamir/ndarray/NdArrayTest.java b/src/test/java/org/sadisamir/ndarray/NdArrayTest.java index 277ad62..2cb7555 100644 --- a/src/test/java/org/sadisamir/ndarray/NdArrayTest.java +++ b/src/test/java/org/sadisamir/ndarray/NdArrayTest.java @@ -372,4 +372,69 @@ void addInPlaceRejectsNullOperand() { assertThrows(NullPointerException.class, () -> left.addInPlace(null)); } + + @Test + void addScalarReturnsNewOneDimensionalArray() { + NdArray source = NdArray.array(new float[] {1.0f, 2.0f, 3.0f}); + + NdArray result = source.add(10.0f); + + assertArrayEquals(new float[] {11.0f, 12.0f, 13.0f}, result.toArray(), 0.0f); + assertArrayEquals(new float[] {1.0f, 2.0f, 3.0f}, source.toArray(), 0.0f); + } + + @Test + void addScalarPreservesTwoDimensionalMetadata() { + NdArray matrix = NdArray.array(new float[][] {{1.0f, 2.0f}, {3.0f, 4.0f}}); + + NdArray result = matrix.add(-1.5f); + + assertEquals(2, result.getNdim()); + assertArrayEquals(new int[] {2, 2}, result.getShape()); + assertEquals(4, result.getSize()); + assertArrayEquals(new float[] {-0.5f, 0.5f}, result.toMatrix()[0], 0.0f); + assertArrayEquals(new float[] {1.5f, 2.5f}, result.toMatrix()[1], 0.0f); + } + + @Test + void addInPlaceScalarMutatesCurrentArray() { + NdArray source = NdArray.array(new float[] {1.0f, 2.0f, 3.0f}); + + source.addInPlace(2.5f); + + assertArrayEquals(new float[] {3.5f, 4.5f, 5.5f}, source.toArray(), 0.0f); + } + + @Test + void addInPlaceScalarPreservesTwoDimensionalShape() { + NdArray matrix = NdArray.array(new float[][] {{1.0f, 2.0f}, {3.0f, 4.0f}}); + + matrix.addInPlace(-1.0f); + + assertEquals(2, matrix.getNdim()); + assertArrayEquals(new int[] {2, 2}, matrix.getShape()); + assertArrayEquals(new float[] {0.0f, 1.0f}, matrix.toMatrix()[0], 0.0f); + assertArrayEquals(new float[] {2.0f, 3.0f}, matrix.toMatrix()[1], 0.0f); + } + + @Test + void sumReturnsTotalForOneDimensionalArray() { + NdArray source = NdArray.array(new float[] {1.0f, 2.5f, 3.5f}); + + assertEquals(7.0f, source.sum(), 0.0f); + } + + @Test + void sumReturnsTotalForTwoDimensionalArray() { + NdArray matrix = NdArray.array(new float[][] {{1.0f, 2.0f}, {3.0f, 4.0f}}); + + assertEquals(10.0f, matrix.sum(), 0.0f); + } + + @Test + void sumOfEmptyArrayIsZero() { + NdArray source = NdArray.array(new float[0]); + + assertEquals(0.0f, source.sum(), 0.0f); + } }