Skip to content

Commit

Permalink
Merge pull request #2 from muandane/feat/calculator
Browse files Browse the repository at this point in the history
added calculation features
  • Loading branch information
muandane authored Oct 18, 2023
2 parents f165299 + 3ad6288 commit 3312ad7
Show file tree
Hide file tree
Showing 9 changed files with 451 additions and 135 deletions.
91 changes: 91 additions & 0 deletions .github/workflows/mega-linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
# MegaLinter GitHub Action configuration file
# More info at https://megalinter.io
name: MegaLinter

on:
# Trigger mega-linter at every push. Action will also be visible from Pull Requests to main
push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions)
pull_request:
branches: [master, main]

env: # Comment env block if you don't want to apply fixes
# Apply linter fixes configuration
APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool)
APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all)
APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request)

concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true

jobs:
megalinter:
name: MegaLinter
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR
# Remove the ones you do not need
contents: write
issues: write
pull-requests: write
steps:
# Git Checkout
- name: Checkout Code
uses: actions/[email protected]
with:
token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances

# MegaLinter
- name: MegaLinter
id: ml
# You can override MegaLinter flavor used to have faster performances
# More info at https://megalinter.io/flavors/
uses: oxsecurity/megalinter/flavors/[email protected]
env:
# All available variables are described in documentation
# https://megalinter.io/configuration/
VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources
GITHUB_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
# ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY
# DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks

# Upload MegaLinter artifacts
- name: Archive production artifacts
if: ${{ success() }} || ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: MegaLinter reports
path: |
megalinter-reports
mega-linter.log
# Create pull request if applicable (for now works only on PR from same repository, not from forks)
- name: Create Pull Request with applied fixes
id: cpr
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.PAT || secrets.PUBLISHER_TOKEN }}
commit-message: "[MegaLinter] Apply linters automatic fixes"
title: "[MegaLinter] Apply linters automatic fixes"
labels: bot
- name: Create PR output
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
# Push new commit if applicable (for now works only on PR from same repository, not from forks)
- name: Prepare commit
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
run: sudo chown -Rc $UID .git/
- name: Commit and push applied linter fixes
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
commit_message: "[MegaLinter] Apply linters fixes"
commit_user_name: megalinter-bot
commit_user_email: [email protected]
4 changes: 2 additions & 2 deletions src/.goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
builds:
- binary: azureprice
- binary: cloudcost
main: ./
goos:
- darwin
Expand All @@ -19,7 +19,7 @@ universal_binaries:
- replace: true

brews:
- name: azureprice
- name: cloudcost
homepage: "https://github.com/muandane/homebrew-gitmoji"
tap:
owner: muandane
Expand Down
41 changes: 41 additions & 0 deletions src/cmd/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Azure Cloud Costs CMD
*/
package cmd

import (
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
)

type Colors struct {
Spot lipgloss.AdaptiveColor
Normal lipgloss.AdaptiveColor
Low lipgloss.AdaptiveColor
}

var vmType string
var region string
var service string
var pricingType string
var currency string
var period int
var bandwidth float64
var typeColors = Colors{
Spot: lipgloss.AdaptiveColor{Light: "#D83F31", Dark: "#D83F31"},
Normal: lipgloss.AdaptiveColor{Light: "#116D6E", Dark: "#00DFA2"},
Low: lipgloss.AdaptiveColor{Light: "#EE9322", Dark: "#E9B824"},
}

// azureCmd represents the azure command
var azureCmd = &cobra.Command{
Use: "azure",
Short: "Main command for Azure resource management.",
Long: `The main command for interacting with Azure resources. This command has subcommands
for searching and calculating the pricing of Azure resources`,
}

func init() {
azureCmd.AddCommand(calculatorCmd)
azureCmd.AddCommand(searchCmd)
}
123 changes: 123 additions & 0 deletions src/cmd/azurecalculator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
Azure Price Calculator CMD
*/
package cmd

import (
"fmt"
"net/url"
"os"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/muandane/azureprice/utils"
"github.com/spf13/cobra"
)

// calculatorCmd represents the calculator command
var calculatorCmd = &cobra.Command{
Use: "calculator",
Short: "Calculate Azure resource pricing based on parameters.",
Long: `Use the azure calculator subcommand to calculate the pricing of an Azure resource.
You can specify the resource name and additional parameters to get accurate pricing details.`,
Run: func(cmd *cobra.Command, args []string) {
re := lipgloss.NewRenderer(os.Stdout)
baseStyle := re.NewStyle().Padding(0, 1)
headerStyle := baseStyle.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#186F65", Dark: "#1AACAC"}).Bold(true)

tableData := [][]string{{"SKU", "Retail Price", "Unit of Measure", "Monthly Price", "Usage", "Region", "Product Name"}}
apiURL := "https://prices.azure.com/api/retail/prices?"
currencyType := fmt.Sprintf("currencyCode='%s'", currency)
query := utils.Query(region, service, vmType, pricingType)
for {
var resp utils.Response
err := utils.GetJSON(apiURL+currencyType+"&$filter="+url.QueryEscape(query), &resp)
if err != nil {
fmt.Println("Error:", err)
return
}

for _, item := range resp.Items {
var usage float64
if strings.Contains(item.UnitOfMeasure, "GB") {
usage = calculateUsageGB(bandwidth, period, item.RetailPrice) // Assuming a bandwidth of 1 GB/day and a span of 30 days
} else if strings.Contains(item.UnitOfMeasure, "Hour") {
usage = calculateUsageHourly(bandwidth, period, item.RetailPrice) // Assuming a bandwidth of 1 GB/day and
} else if strings.Contains(item.UnitOfMeasure, "Month") {
usage = calculateUsageMonthly(bandwidth, period, item.RetailPrice) // Assuming a bandwidth of 1 GB/day and
}

var monthlyPrice string
if pricingType != "Reservation" && !strings.Contains(item.UnitOfMeasure, "GB") && !strings.Contains(item.UnitOfMeasure, "Month") {
monthlyPrice = fmt.Sprintf("%v", item.RetailPrice*730) // Calculate the monthly price
} else {
monthlyPrice = "---"
}
tableData = append(tableData, []string{item.ArmSkuName, fmt.Sprintf("%f", item.RetailPrice), item.UnitOfMeasure, fmt.Sprintf("%v", monthlyPrice), fmt.Sprintf("%f", usage), item.ArmRegionName, item.MeterName, item.ProductName})
}
if resp.NextPageLink == "" {
break
}
apiURL = resp.NextPageLink
}
headers := []string{"SKU", "Retail Price", "Unit of Measure", "Monthly Price", "Usage", "Region", "Product Name"}
CapitalizeHeaders := func(tableData []string) []string {
for i := range tableData {
tableData[i] = strings.ToUpper(tableData[i])
}
return tableData
}
t := table.New().
Border(lipgloss.NormalBorder()).
BorderStyle(re.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#186F65", Dark: "#1AACAC"})).
Headers(CapitalizeHeaders(headers)...).
Width(120).
Rows(tableData[1:]...). // Pass only the rows to the Rows function
StyleFunc(func(row, col int) lipgloss.Style {
if row == 0 {
return headerStyle
}
if col == 4 {
// Check if the "Meter" column contains "Spot" or "Low"
meter := tableData[row-0][4] // The "Meter" column is the 5th column (index 4)
color := lipgloss.AdaptiveColor{Light: "#186F65", Dark: "#1AACAC"} // Default color
if strings.Contains(meter, "Spot") {
color = typeColors.Spot
} else if strings.Contains(meter, "Low") {
color = typeColors.Low
} else {
color = typeColors.Normal
}
return baseStyle.Copy().Foreground(color)
}
return baseStyle.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#053B50", Dark: "#F1EFEF"})
})
fmt.Println(t)
},
}

func init() {
calculatorCmd.Flags().StringVarP(&vmType, "type", "t", "", "VM type")
calculatorCmd.Flags().StringVarP(&region, "region", "r", "", "Region")
calculatorCmd.Flags().StringVarP(&service, "service", "s", "", "Azure service (e.g., 'D' for D series vms, Private for Private links)")
calculatorCmd.Flags().StringVarP(&pricingType, "pricing-type", "p", "Consumption", "Pricing Type (e.g., 'Consumption' or 'Reservation')")
calculatorCmd.Flags().StringVarP(&currency, "currency", "c", "", "Price Currency (e.g., 'USD' or 'EUR')")
calculatorCmd.Flags().Float64VarP(&bandwidth, "bandwidth", "b", 1, "Pricing Type (e.g., 'Consumption' or 'Reservation')")
calculatorCmd.Flags().IntVarP(&period, "days", "d", 1, "period (e.g., '1' for 1 day, '7' for 7 days)")
}

func calculateUsageGB(bandwidth float64, days int, usagePerGB float64) float64 {
totalUsage := bandwidth * float64(days) * usagePerGB
return totalUsage
}
func calculateUsageHourly(period float64, days int, usagePerHour float64) float64 {
hours := days * 24 // convert days to hours
totalUsage := period * float64(hours) * usagePerHour
return totalUsage
}
func calculateUsageMonthly(period float64, days int, usagePerMonth float64) float64 {
month := days / 30 // convert days to hours
totalUsage := period * float64(month) * usagePerMonth
return totalUsage
}
100 changes: 100 additions & 0 deletions src/cmd/azuresearch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Azure Price Search CMD
*/
package cmd

import (
"fmt"
"net/url"
"os"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/muandane/azureprice/utils"
"github.com/spf13/cobra"
)

func init() {
searchCmd.Flags().StringVarP(&vmType, "type", "t", "", "VM type")
searchCmd.Flags().StringVarP(&region, "region", "r", "", "Region")
searchCmd.Flags().StringVarP(&service, "service", "s", "", "Azure service (e.g., 'D' for D series vms, Private for Private links)")
searchCmd.Flags().StringVarP(&pricingType, "pricing-type", "p", "Consumption", "Pricing Type (e.g., 'Consumption' or 'Reservation')")
searchCmd.Flags().StringVarP(&currency, "currency", "c", "", "Price Currency (e.g., 'USD' or 'EUR')")
}

var searchCmd = &cobra.Command{
Use: "search",
Short: "Search for an Azure resource and get pricing information.",
Long: `Use the azure search subcommand to search for a specific Azure resource
and retrieve its pricing information. Provide the resource name
as an argument to this command.`,
Run: func(cmd *cobra.Command, args []string) {
re := lipgloss.NewRenderer(os.Stdout)
baseStyle := re.NewStyle().Padding(0, 1)
headerStyle := baseStyle.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#186F65", Dark: "#1AACAC"}).Bold(true)

tableData := [][]string{{"SKU", "Retail Price", "Unit of Measure", "Monthly Price", "Region", "Meter", "Product Name"}}
apiURL := "https://prices.azure.com/api/retail/prices?"
currencyType := fmt.Sprintf("currencyCode='%s'", currency)
query := utils.Query(region, service, vmType, pricingType)
escapedQuery := url.QueryEscape(query)
for {
var resp utils.Response
err := utils.GetJSON(apiURL+currencyType+"&$filter="+escapedQuery, &resp)
if err != nil {
fmt.Println("Error:", err)
return
}

for _, item := range resp.Items {
var monthlyPrice string
if pricingType != "Reservation" && !strings.Contains(item.UnitOfMeasure, "GB") && !strings.Contains(item.UnitOfMeasure, "Month") {
monthlyPrice = fmt.Sprintf("%v", item.RetailPrice*730) // Calculate the monthly price
} else {
monthlyPrice = "---"
}
tableData = append(tableData, []string{item.ArmSkuName, fmt.Sprintf("%f", item.RetailPrice), item.UnitOfMeasure, fmt.Sprintf("%v", monthlyPrice), item.MeterName, item.ArmRegionName, item.ProductName})
}
if resp.NextPageLink == "" {
break
}
apiURL = resp.NextPageLink
}

headers := []string{"SKU", "Retail Price", "Unit of Measure", "Monthly Price", "Meter", "Region", "Product Name"}
CapitalizeHeaders := func(tableData []string) []string {
for i := range tableData {
tableData[i] = strings.ToUpper(tableData[i])
}
return tableData
}

t := table.New().
Border(lipgloss.NormalBorder()).
BorderStyle(re.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#186F65", Dark: "#1AACAC"})).
Headers(CapitalizeHeaders(headers)...).
Width(120).
Rows(tableData[1:]...). // Pass only the rows to the Rows function
StyleFunc(func(row, col int) lipgloss.Style {
if row == 0 {
return headerStyle
}
if col == 4 {
// Check if the "Meter" column contains "Spot" or "Low"
meter := tableData[row-0][4] // The "Meter" column is the 5th column (index 4)
color := lipgloss.AdaptiveColor{Light: "#186F65", Dark: "#1AACAC"} // Default color
if strings.Contains(meter, "Spot") {
color = typeColors.Spot
} else if strings.Contains(meter, "Low") {
color = typeColors.Low
} else {
color = typeColors.Normal
}
return baseStyle.Copy().Foreground(color)
}
return baseStyle.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#053B50", Dark: "#F1EFEF"})
})
fmt.Println(t)
},
}
Loading

0 comments on commit 3312ad7

Please sign in to comment.