Home
Softono
gmig

gmig

Open source MIT Go
27
Stars
10
Forks
4
Issues
1
Watchers
1 year
Last Commit

About gmig

Google Cloud Platform migrations tool for infrastructure-as-code

Platforms

Web Self-hosted Cloud

Languages

Go

Links

gmig - GCP migrations

pronounced as gimmick.

Build Status Go Report Card GoDoc

Manage Google Cloud Platform (GCP) infrastructure using migrations that describe incremental changes such as additions or deletions of resources. This work is inspired by MyBatis migrations for SQL database setup.

Introduction blog post

Your gmig infrastructure is basically a folder with incremental change files, each with a timestamp prefix (for sort ordering) and readable name.

/010_create_some_account.yaml
/015_add_permissions_to_some_account.yaml
/my-gcp-production-project
    gmig.yaml

Each change is a single YAML file with one or more shell commands that change infrastructure for a project.

# add loadrunner service account

do:
- gcloud iam service-accounts create loadrunner --display-name "LoadRunner"

undo:
- gcloud iam service-accounts delete loadrunner

A change must have at least a do section and optionally an undo section. The do section typically has a list of gcloud commands that create resources but any available tool can be used. All lines will be executed at once using a single temporary shell script so you can use shell variables to simplify each section. The undo section typically has an ordered list of gcloud commands that deletes the same resources (in reverse order if relevant). Each command in each section can use the following environment variables: $PROJECT,$REGION,$ZONE,$GMIG_CONFIG_DIR, and any additional environment variables populated from the target configuration (see env section in the configuration below).

State

Information about the last applied migration to a project is stored as a Google Storage Bucket object. Therefore, usage of this tool requires you to have create a Bucket and set the permissions (Storage Writer) accordingly. To view the current state of your infrastructure related to each migration, you can add the view section to the YAML file, such as:

# add loadrunner service account

do:
- gcloud iam service-accounts create loadrunner --display-name "LoadRunner"

undo:
- gcloud iam service-accounts delete loadrunner

view:
- gcloud iam service-accounts describe loadrunner

and use the view subcommand.

Conditional migration

Commands (do,undo,view) can be made conditional by adding an if section. You can only use custom environment variables and configuration parameters (PROJECT,ZONE,REGION) in expressions. If the expression evaluates to true then the do (up), undo (down) and view (view) commands are executed.

if: PROJECT == "your-project-id"
do:
- gcloud condig list

or with combinations:

if: (PROJECT == "your-project-id") && (ZONE == "my-zone")
do:
- gcloud condig list

For available operators, see Language-Definition

Help

NAME:
gmig - Google Cloud Platform infrastructure migration tool

USAGE:
gmig [global options] command [command options] [arguments...]

COMMANDS:
    init     Create the initial configuration, if absent.
    new      Create a new migration file from a template using a generated timestamp and a given title.
    up       Runs the do section of all pending migrations in order, one after the other.
             If a migration file is specified then stop after applying that one.
    down     Runs the undo section of the last applied migration only.
    down-all Runs the undo section of all applied migrations.
    plan     Log commands of the do section of all pending migrations in order, one after the other.
    status   List all migrations with details compared to the current state.
    view     Runs the view section of all applied migrations to see the current state reported by your infrastructure.
    force    state | do | undo
    util     create-named-port | delete-named-port
    export   project-iam-policy | storage-iam-policy
    help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
-q                   quiet mode, accept any prompt
-v                   verbose logging
--help, -h           show help
--print-version, -V  print only the version

Getting started

Installation

You need to compile it using the Go SDK.

go install github.com/emicklei/gmig@latest

init \

Prepares your setup for working with migrations by creating a gmig.json file in a target folder.

gmig init my-gcp-production-project

Then your filesystem will have:

/my-gcp-production-project/
    gmig.yaml

You must change the file gmig.yaml to set the Project and Bucket name.

# gmig configuration file
#
# Google Cloud Platform migrations tool for infrastructure-as-code. See https://github.com/emicklei/gmig.

# [project] must be the Google Cloud Project ID where the infrastructure is created.
# Its value is available as $PROJECT in your migrations.
#
# Required by gmig.
project: my-project

# [region] must be a valid GCP region. See https://cloud.google.com/compute/docs/regions-zones/
# A region is a specific geographical location where you can run your resources.
# Its value is available as $REGION in your migrations.
#
# Not required by gmig but some gcloud and gsutil commands do require it.
# region: europe-west1

# [zone] must be a valid GCP zone. See https://cloud.google.com/compute/docs/regions-zones/
# Each region has one or more zones; most regions have three or more zones.
# Its value is available as $ZONE in your migrations.
#
# Not required by gmig but some gcloud and gsutil commands do require it.
# zone: europe-west1-b

# [bucket] must be a valid GCP Storage bucket.
# A Google Storage Bucket is used to store information (object) about the last applied migration.
# Bucket can contain multiple objects from multiple applications. Make sure the [state] is different for each app.
#
# Required by gmig.
bucket: my-bucket

# [state] is the name of the object that hold information about the last applied migration.
# Required by gmig.
state: myapp-gmig-last-migration

# [env] are additional environment values that are available to each section of a migration file.
# This can be used to create migrations that are independent of the target project.
# By convention, use capitalized words for keys.
# In the example, "myapp-cluster" is available as $K8S_CLUSTER in your migrations.
#
# Not required by gmig.
env:
  K8S_CLUSTER: myapp-cluster

If you decide to store state files of different projects in one Bucket then set the state object name to reflect this, eg. myproject-gmig-state. If you want to apply the same migrations to different regions/zones then choose a target folder name to reflect this, eg. my-gcp-production-project-us-east. Values for region and zone are required if you want to create Compute Engine resources. The env map can be used to parameterize commands in your migrations. In the example, all commands will have access to the value of $K8S_CLUSTER.

new \</h3> <p>Creates a new migration for you to describe a change to the current state of infrastructure.</p> <pre><code>gmig new "add storage view role to cloudbuild account"</code></pre> <p>Using a combination of the options <code>--do</code>, <code>--undo</code> and <code>--view</code>, you can set the commands directly for the new migration.</p> <h3>status \<path> [--migrations folder]</h3> <p>List all migrations with an indicator (applied,pending) whether is has been applied or not.</p> <pre><code>gmig status my-gcp-production-project/</code></pre> <p>Run this command in the directory where all migrations are stored. Use <code>--migrations</code> for a different location.</p> <h3>plan \<path> [stop] [--migrations folder]</h3> <p>Log commands of the <code>do</code> section of all pending migrations in order, one after the other. If <code>stop</code> is given, then stop after that migration file.</p> <h3>up \<path> [stop] [--migrations folder]</h3> <p>Executes the <code>do</code> section of each pending migration compared to the last applied change to the infrastructure. If <code>stop</code> is given, then stop after that migration file. Upon each completed migration, the <code>gmig-last-migration</code> object is updated in the bucket.</p> <pre><code>gmig up my-gcp-production-project</code></pre> <h3>down \<path> [--migrations folder]</h3> <p>Executes one <code>undo</code> section of the last applied change to the infrastructure. If completed then update the <code>gmig-last-migration</code> object.</p> <pre><code>gmig down my-gcp-production-project</code></pre> <h3>down-all \<path> [--migrations folder]</h3> <p>Executes <code>undo</code> section of all applied change to the infrastructure. Updates the <code>gmig-last-migration</code> object after each successfull step.</p> <pre><code>gmig down-all my-gcp-production-project</code></pre> <h3>view \<path> [migration file] [--migrations folder]</h3> <p>Executes the <code>view</code> section of each applied migration to the infrastructure. If <code>migration file</code> is given then run that view only.</p> <pre><code>gmig view my-gcp-production-project</code></pre> <h3>template [-w] source-file</h3> <p>Processes the source-file as a Go template and write the result to stdout. If the <code>-w</code> is given then rewrite the source with the processed content. The following functions are available:</p> <h4>env</h4> <p>This function takes the first argument and does a lookup in the available OS environment values. Example of a configuration snippet that needs the environment dependent value for $PROJECT.</p> <pre><code>project: {{ env "PROJECT" }}</code></pre> <p>Example:</p> <pre><code>gmig template some-config.template.yaml > some-config.yaml</code></pre> <h2>Export existing infrastructure</h2> <p>Exporting migrations from existing infrastructure is useful when you start working with <code>gmig</code> but do not want to start from scratch. Several sub commands are (or will become) available to inspect a project and export migrations to reflect the current state. After marking the current state in <code>gmig</code> (using <code>force-state</code>), new migrations can be added that will bring your infrastructure to the next state. The generated migration can ofcourse also be used to just copy commands to your own migration.</p> <h3>export project-iam-policy \<path></h3> <p>Generate a new migration by reading all the IAM policy bindings from the current infrastructure of the project.</p> <pre><code>gmig -v export project-iam-policy my-project/</code></pre> <h3>export storage-iam-policy \<path></h3> <p>Generate a new migration by reading all the IAM policy bindings, per Google Storage Bucket owned by the project.</p> <pre><code>gmig -v export storage-iam-policy my-project/</code></pre> <h2>Working around migrations</h2> <p>Sometimes you need to fix things because you made a mistake or want to reorganise your work. Use the <code>force</code> and confirm your action.</p> <h3>force state \<path> \<filename></h3> <p>Explicitly set the state for the target to the last applied filename. This command can be useful if you need to work from existing infrastructure. Effectively, this filename is written to the bucket object. Use this command with care!.</p> <pre><code>gmig force state my-gcp-production-project 010_create_some_account.yaml</code></pre> <h3>force do \<path> \<filename></h3> <p>Explicitly run the commands in the <code>do</code> section of a given migration filename. The <code>gmig-last-migration</code> object is <code>not</code> updated in the bucket. Use this command with care!.</p> <pre><code>gmig force do my-gcp-production-project 010_create_some_account.yaml</code></pre> <h3>force undo \<path> \<filename></h3> <p>Explicitly run the commands in the <code>undo</code> section of a given migration filename. The <code>gmig-last-migration</code> object is <code>not</code> updated in the bucket. Use this command with care!.</p> <pre><code>gmig force undo my-gcp-production-project 010_create_some_account.yaml</code></pre> <h2>export-env \<path></h2> <p>Export all available environment variable from the configuration file and also export $PROJECT, $REGION and $ZONE Use this command with care!.</p> <pre><code>eval $(gmig export-env my-gcp-production-project)</code></pre> <h2>GCP utilities</h2> <h3>util create-named-port \<instance-group> \<name:port></h3> <p>The Cloud SDK has a command to <a href="https://cloud.google.com/sdk/gcloud/reference/compute/instance-groups/set-named-ports">set-named-ports</a> but not a command to add or delete a single name:port mapping. To simplify the migration command for creating a name:port mapping, this gmig util command is added. First it calls <code>get-named-ports</code> to retrieve all existing mappings. Then it will call <code>set-named-ports</code> with the new mapping unless it already exists.</p> <h3>util delete-named-port \<instance-group> \<name:port></h3> <p>The Cloud SDK has a command to <a href="https://cloud.google.com/sdk/gcloud/reference/compute/instance-groups/set-named-ports">set-named-ports</a> but not a command to add or delete a single name:port mapping. To simplify the migration command for deleting a name:port mapping, this <code>gmig</code> util command is added. First it calls <code>get-named-ports</code> to retrieve all existing mappings. Then it will call <code>set-named-ports</code> without the mapping.</p> <h3>util add-path-rules-to-path-matcher [config folder] -url-map [url-map-name] -service [backend-service-name] -path-matcher [path-matcher-name] -paths "/v1/path/<em>, /v1/otherpath/</em>"</h3> <p>The Cloud SDK has a command to <a href="https://cloud.google.com/sdk/gcloud/reference/compute/url-maps/add-path-matcher">add a patch matcher</a> with a set of paths but not a command update the path rules of an existing path matcher in the url map. To write a migration that changes the set of paths (add,remove), this <code>gmig</code> util command is added. First is <a href="https://cloud.google.com/sdk/gcloud/reference/compute/url-maps/export">exports</a> an URL map, updates the paths of the rules of a path-matcher, then imports the changed URL map. Because this migration is changing a regional resource which is typically shared by multiple services, the patching of the URL map is executed using a global lock (using the Bucket from the config).</p> <h2>Examples</h2> <p>This repository has a number of <a href="https://github.com/emicklei/gmig/tree/master/examples">examples</a> of migrations.</p> <p>© 2022, ernestmicklei.com. MIT License. Contributions welcome.</p> </article> </div> </div> </div> </div> </section> </div> </div> </main> <!-- ========================= Footer v3 ===========================--> <footer class="footer footer-three dark:bg-background-8 {=$class} relative overflow-hidden bg-white"> <div class="main-container"> <div class="grid grid-cols-12 justify-between gap-x-0 gap-y-16 pt-16 pb-16 lg:gap-x-8 xl:gap-x-0 xl:pt-[100px]"> <div class="col-span-12 lg:col-span-4"> <div data-ns-animate data-delay="0.3" class="xl:max-w-[306px]"> <figure> <img src="https://img.softono.com/qoj701ib3Ld4bDgb-icIWTSfvTWeTYajWDUdTPwHgQ0/aHR0cHM6Ly9zb2Z0b25vLmNvbS91cGxvYWQvbG9nby9sb2dvLnBuZw" class="dark:hidden" alt="Nexsass" /> <img src="https://img.softono.com/qoj701ib3Ld4bDgb-icIWTSfvTWeTYajWDUdTPwHgQ0/aHR0cHM6Ly9zb2Z0b25vLmNvbS91cGxvYWQvbG9nby9sb2dvLnBuZw" class="hidden dark:block" alt="Nexsass" /> </figure> <p class="text-secondary dark:text-accent mt-4 mb-7"> Turpis tortor nunc sed amet et faucibus vitae morbi congue sed id mauris. </p> <div class="flex items-center gap-3"> <a href="#" class="footer-social-link"> <span class="sr-only">Facebook</span> <svg xmlns="http://www.w3.org/2000/svg" width="7" height="16" viewBox="0 0 7 16" fill="none"> <path d="M2.25 15C2.25 15.4142 2.58579 15.75 3 15.75C3.41421 15.75 3.75 15.4142 3.75 15H2.25ZM3.75 7C3.75 6.58579 3.41421 6.25 3 6.25C2.58579 6.25 2.25 6.58579 2.25 7H3.75ZM6 1.75C6.41421 1.75 6.75 1.41421 6.75 1C6.75 0.585786 6.41421 0.25 6 0.25V1.75ZM3 4H2.25H3ZM2.25 7C2.25 7.41421 2.58579 7.75 3 7.75C3.41421 7.75 3.75 7.41421 3.75 7H2.25ZM3 6.25C2.58579 6.25 2.25 6.58579 2.25 7C2.25 7.41421 2.58579 7.75 3 7.75V6.25ZM5 7.75C5.41421 7.75 5.75 7.41421 5.75 7C5.75 6.58579 5.41421 6.25 5 6.25V7.75ZM3 7.75C3.41421 7.75 3.75 7.41421 3.75 7C3.75 6.58579 3.41421 6.25 3 6.25V7.75ZM1 6.25C0.585786 6.25 0.25 6.58579 0.25 7C0.25 7.41421 0.585786 7.75 1 7.75V6.25ZM3 15H3.75V7H3H2.25V15H3ZM6 1V0.25C3.92893 0.25 2.25 1.92893 2.25 4H3H3.75C3.75 2.75736 4.75736 1.75 6 1.75V1ZM3 4H2.25V7H3H3.75V4H3ZM3 7V7.75H5V7V6.25H3V7ZM3 7V6.25H1V7V7.75H3V7Z" class="fill-secondary dark:fill-accent" /> </svg> </a> <div class="bg-stroke-1 dark:bg-stroke-8 h-5 w-px"></div> <a href="#" class="footer-social-link"> <span class="sr-only">Instagram</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"> <path fill-rule="evenodd" clip-rule="evenodd" d="M11 1H5C2.79086 1 1 2.79086 1 5V11C1 13.2091 2.79086 15 5 15H11C13.2091 15 15 13.2091 15 11V5C15 2.79086 13.2091 1 11 1Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path fill-rule="evenodd" clip-rule="evenodd" d="M8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8C11 8.79565 10.6839 9.55871 10.1213 10.1213C9.55871 10.6839 8.79565 11 8 11Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <rect x="11" y="5" width="2" height="2" rx="1" transform="rotate(-90 11 5)" class="fill-secondary dark:fill-accent" /> <rect x="11.5" y="4.5" width="1" height="1" rx="0.5" transform="rotate(-90 11.5 4.5)" class="stroke-secondary dark:stroke-accent" stroke-linecap="round" /> </svg> </a> <div class="bg-stroke-1 dark:bg-stroke-8 h-5 w-px"></div> <a href="#" class="footer-social-link"> <span class="sr-only">Youtube</span> <svg xmlns="http://www.w3.org/2000/svg" width="22" height="16" viewBox="0 0 22 16" fill="none"> <path fill-rule="evenodd" clip-rule="evenodd" d="M16.668 15.0028C18.9724 15.0867 20.91 13.29 21 10.9858V5.01982C20.91 2.71569 18.9724 0.918929 16.668 1.00282H5.332C3.02763 0.918929 1.08998 2.71569 1 5.01982V10.9858C1.08998 13.29 3.02763 15.0867 5.332 15.0028H16.668Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path fill-rule="evenodd" clip-rule="evenodd" d="M10.508 5.17711L13.669 7.32511C13.8738 7.44468 13.9997 7.66398 13.9997 7.90111C13.9997 8.13824 13.8738 8.35754 13.669 8.47711L10.508 10.8271C9.908 11.2341 9 10.8871 9 10.2511V5.75111C9 5.11811 9.909 4.77011 10.508 5.17711Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> </svg> </a> <div class="bg-stroke-1 dark:bg-stroke-8 h-5 w-px"></div> <a href="#" class="footer-social-link"> <span class="sr-only">LinkedIn</span> <svg xmlns="http://www.w3.org/2000/svg" width="13" height="11" viewBox="0 0 13 11" fill="none"> <path d="M2.25 4C2.25 3.58579 1.91421 3.25 1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4H2.25ZM0.75 10C0.75 10.4142 1.08579 10.75 1.5 10.75C1.91421 10.75 2.25 10.4142 2.25 10H0.75ZM10.75 10C10.75 10.4142 11.0858 10.75 11.5 10.75C11.9142 10.75 12.25 10.4142 12.25 10H10.75ZM5.5 7H4.75H5.5ZM4.75 10C4.75 10.4142 5.08579 10.75 5.5 10.75C5.91421 10.75 6.25 10.4142 6.25 10H4.75ZM2.25 1C2.25 0.585786 1.91421 0.25 1.5 0.25C1.08579 0.25 0.75 0.585786 0.75 1H2.25ZM0.75 2C0.75 2.41421 1.08579 2.75 1.5 2.75C1.91421 2.75 2.25 2.41421 2.25 2H0.75ZM1.5 4H0.75V10H1.5H2.25V4H1.5ZM11.5 10H12.25V7H11.5H10.75V10H11.5ZM11.5 7H12.25C12.25 4.92893 10.5711 3.25 8.5 3.25V4V4.75C9.74264 4.75 10.75 5.75736 10.75 7H11.5ZM8.5 4V3.25C6.42893 3.25 4.75 4.92893 4.75 7H5.5H6.25C6.25 5.75736 7.25736 4.75 8.5 4.75V4ZM5.5 7H4.75V10H5.5H6.25V7H5.5ZM1.5 1H0.75V2H1.5H2.25V1H1.5Z" class="fill-secondary dark:fill-accent" /> </svg> </a> <div class="bg-stroke-1 dark:bg-stroke-8 h-5 w-px"></div> <a href="#" class="footer-social-link"> <span class="sr-only">Dribbble</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.81146 14.7617C6.69789 15.5957 3.41731 14.1957 1.86521 11.3707C0.313116 8.54567 0.890795 5.02595 3.26447 2.84524C5.63814 0.66452 9.19411 0.386619 11.8777 2.1721C14.5614 3.95759 15.6788 7.34483 14.5845 10.3767C13.8079 12.532 12.0248 14.1702 9.81146 14.7617Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M9.06142 14.7162C9.03653 15.1297 9.35153 15.485 9.765 15.5099C10.1785 15.5348 10.5338 15.2198 10.5587 14.8063L9.06142 14.7162ZM6.84286 0.874373C6.64188 0.512186 6.18534 0.381502 5.82315 0.582483C5.46097 0.783464 5.33028 1.24 5.53126 1.60219L6.84286 0.874373ZM13.2187 2.9035C13.3591 2.5138 13.157 2.08408 12.7673 1.94368C12.3776 1.80328 11.9479 2.00537 11.8075 2.39506L13.2187 2.9035ZM7.74006 7.03428L7.54644 6.30971L7.54546 6.30997L7.74006 7.03428ZM1.89802 5.05032C1.58158 4.78304 1.10838 4.82289 0.841101 5.13932C0.573819 5.45576 0.613667 5.92896 0.930105 6.19624L1.89802 5.05032ZM2.77955 13.0958C2.63901 13.4855 2.84095 13.9153 3.23059 14.0558C3.62023 14.1963 4.05003 13.9944 4.19057 13.6048L2.77955 13.0958ZM8.25822 8.96384L8.06412 8.23939L8.25822 8.96384ZM14.1013 10.9494C14.4178 11.2166 14.891 11.1766 15.1582 10.8601C15.4254 10.5435 15.3854 10.0703 15.0688 9.80317L14.1013 10.9494ZM9.81006 14.7613L10.5587 14.8063C10.7186 12.1509 10.1178 9.27114 9.32769 6.78072C8.53534 4.28333 7.53363 2.11922 6.84286 0.874373L6.18706 1.23828L5.53126 1.60219C6.17449 2.76135 7.13628 4.83373 7.89793 7.23434C8.66179 9.64192 9.20557 12.3216 9.06142 14.7162L9.81006 14.7613ZM12.5131 2.64928L11.8075 2.39506C11.1142 4.31922 9.52233 5.7817 7.54644 6.30971L7.74006 7.03428L7.93369 7.75886C10.3844 7.10397 12.3588 5.29004 13.2187 2.9035L12.5131 2.64928ZM7.74006 7.03428L7.54546 6.30997C5.57029 6.84064 3.46046 6.37005 1.89802 5.05032L1.41406 5.62328L0.930105 6.19624C2.86801 7.83311 5.48485 8.41679 7.93467 7.75859L7.74006 7.03428ZM3.48506 13.3503L4.19057 13.6048C4.88464 11.6805 6.47642 10.2177 8.45232 9.68829L8.25822 8.96384L8.06412 8.23939C5.614 8.89585 3.64019 10.7097 2.77955 13.0958L3.48506 13.3503ZM8.25822 8.96384L8.45232 9.68829C10.4282 9.15889 12.5381 9.62992 14.1013 10.9494L14.5851 10.3763L15.0688 9.80317C13.1305 8.16701 10.5142 7.58293 8.06412 8.23939L8.25822 8.96384Z" class="fill-secondary dark:fill-accent" /> </svg> </a> <div class="bg-stroke-1 dark:bg-stroke-8 h-5 w-px"></div> <a href="#" class="footer-social-link"> <span class="sr-only">Behance</span> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="14" viewBox="0 0 16 14" fill="none"> <path fill-rule="evenodd" clip-rule="evenodd" d="M1 7V1H4C5.65685 1 7 2.34315 7 4C7 5.65685 5.65685 7 4 7C5.65685 7 7 8.34315 7 10C7 11.6569 5.65685 13 4 13H1V7Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path fill-rule="evenodd" clip-rule="evenodd" d="M15 10C15 8.34315 13.6569 7 12 7C10.3431 7 9 8.34315 9 10H15Z" class="stroke-secondary dark:stroke-accent" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /> <path d="M1 6.25C0.585786 6.25 0.25 6.58579 0.25 7C0.25 7.41421 0.585786 7.75 1 7.75V6.25ZM4 7.75C4.41421 7.75 4.75 7.41421 4.75 7C4.75 6.58579 4.41421 6.25 4 6.25V7.75ZM9.75 9.99998C9.74999 9.58577 9.41419 9.24999 8.99998 9.25C8.58577 9.25001 8.24999 9.58581 8.25 10L9.75 9.99998ZM10.9295 12.8024L10.6619 13.5031L10.9295 12.8024ZM14.795 12.5C15.0712 12.1913 15.0447 11.7172 14.736 11.441C14.4273 11.1648 13.9532 11.1913 13.677 11.5L14.795 12.5ZM14 5.75C14.4142 5.75 14.75 5.41421 14.75 5C14.75 4.58579 14.4142 4.25 14 4.25V5.75ZM10 4.25C9.58579 4.25 9.25 4.58579 9.25 5C9.25 5.41421 9.58579 5.75 10 5.75V4.25ZM1 7V7.75H4V7V6.25H1V7ZM9 10L8.25 10C8.25004 11.5548 9.20948 12.9483 10.6619 13.5031L10.9295 12.8024L11.1971 12.1018C10.3257 11.7689 9.75002 10.9328 9.75 9.99998L9 10ZM10.9295 12.8024L10.6619 13.5031C12.1143 14.0578 13.7584 13.6588 14.795 12.5L14.236 12L13.677 11.5C13.0551 12.1953 12.0686 12.4347 11.1971 12.1018L10.9295 12.8024ZM14 5V4.25H10V5V5.75H14V5Z" class="fill-secondary dark:fill-accent" /> </svg> </a> </div> </div> </div> <div class="col-span-12 grid grid-cols-12 gap-x-0 gap-y-8 lg:col-span-8"> <div class="col-span-12 md:col-span-4"> <div data-ns-animate data-delay="0.4" class="space-y-8"> <p class="sm:text-heading-6 text-tagline-1 text-secondary dark:text-accent font-normal"> Company </p> <ul class="space-y-3"> <li> <a href="page/about-us" class="footer-link-v2 router pjax"> About Us </a> </li> <li> <a href="himanshu" class="footer-link-v2 router pjax"> About Founder </a> </li> <li> <a href="services" class="footer-link-v2 router pjax"> Our Services </a> </li> <li> <a href="testimonials" class="footer-link-v2 router pjax"> Testimonials </a> </li> <li> <a href="contact" class="footer-link-v2 router pjax"> Contact Us </a> </li> </ul> </div> </div> <div class="col-span-12 md:col-span-4"> <div data-ns-animate data-delay="0.5" class="space-y-8"> <p class="sm:text-heading-6 text-tagline-1 text-secondary dark:text-accent font-normal"> Explore </p> <ul class="space-y-3"> <li> <a href="softwares" class="footer-link-v2 router pjax"> Softwares </a> </li> <li> <a href="vendors" class="footer-link-v2 router pjax"> Software Vendors </a> </li> <li> <a href="softwares/categories" class="footer-link-v2 router pjax"> Software Categories </a> </li> <li> <a href="blog" class="footer-link-v2 router pjax"> Tech Blog </a> </li> </ul> </div> </div> <div class="col-span-12 md:col-span-4"> <div data-ns-animate data-delay="0.6" class="space-y-8"> <p class="sm:text-heading-6 text-tagline-1 text-secondary dark:text-accent font-normal"> Legal Policies </p> <ul class="space-y-3"> <li> <a href="page/terms-condition" class="footer-link-v2 router pjax"> Terms & Conditions </a> </li> <li> <a href="page/privacy-policy" class="footer-link-v2 router pjax"> Privacy Policy </a> </li> </ul> </div> </div> </div> </div> <div class="relative overflow-hidden pt-6 pb-[60px] text-center"> <div class="footer-divider bg-stroke-2 dark:bg-accent/5 absolute top-0 right-0 left-0 mx-auto h-px w-0 origin-center"></div> <p data-ns-animate data-delay="0.7" data-offset="10" data-start="top 105%" class="text-secondary dark:text-accent/60"> ©2026 , made by <a href="http://softono.com" target="_blank" class="text-secondary dark:text-accent">Softono</a> </p> </div> </div> <div class="{=$hide-theme-toggle}"> <!-- ========================= Theme Toggle Button ===========================--> <button id="theme-toggle" data-default-theme="{=$default-theme}" aria-label="Theme toggle button" class="size-12 bg-background-8 !z-[9999] dark:bg-white rounded-l-2xl cursor-pointer flex items-center justify-center fixed right-0 bottom-5"> <span id="dark-theme-icon"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 stroke-black"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" /> </svg> </span> <span id="light-theme-icon"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="size-6 stroke-white"> <path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" /> </svg> </span> </button> </div> <div id="common-modal" class="modal fade"> <div class="modal-dialog"> <div class="modal-content" id="common-modal-content"> </div> </div> </div> </footer> <script src="https://code.jquery.com/jquery-4.0.0.min.js" integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script> <script src="theme/vendor/swiper.min.js"></script> <script src="theme/vendor/leaflet.min.js"></script> <script src="theme/vendor/vanilla-infinite-marquee.min.js"></script> <script src="theme/vendor/split-text.min.js"></script> <script src="theme/vendor/gsap.min.js"></script> <script src="theme/vendor/scroll-trigger.min.js"></script> <script src="theme/vendor/draw-svg.min.js"></script> <script src="theme/vendor/scroll-trigger.min.js"></script> <script src="theme/vendor/motionpathplugin.min.js"></script> <script src="theme/vendor/lenis.min.js"></script> <script src="theme/vendor/springer.min.js"></script> <script src="theme/vendor/number-counter.js"></script> <script src="theme/vendor/stack-card.min.js"></script> <script src="theme/assets/main.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.21.0/jquery.validate.min.js" integrity="sha512-KFHXdr2oObHKI9w4Hv1XPKc898mE4kgYx58oqsc/JqqdLMDI4YjOLzom+EMlW8HFUd0QfjfAvxSL6sEq/a42fQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="assets/js/app.js?v=4"></script> <script src="assets/js/pjax.js?v=8"></script> <script src="assets/js/common.js"></script> <script> $(document).ready(function() { pjax.onLinkClick = function(target){ updateActiveMenu(target); }; pjax.onPageLoaded = function(url){ initRevealElements(); }; pjax.init(); }); </script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.css"> <script type="module"> import 'https://cdn.jsdelivr.net/gh/orestbida/cookieconsent@3.0.1/dist/cookieconsent.umd.js'; window.addEventListener('load', function() { if (window.templateCustomizer?.settings?.style === 'dark') { document.documentElement.classList.add('cc--darkmode'); } CookieConsent.run({ // root: 'body', // autoShow: true, //disablePageInteraction: true, // hideFromBots: true, // mode: 'opt-in', //revision: 100, cookie: { name: 'cc_cookie', // domain: location.hostname, // path: '/', // sameSite: "Lax", expiresAfterDays: 365, }, // https://cookieconsent.orestbida.com/reference/configuration-reference.html#guioptions guiOptions: { consentModal: { layout: 'cloud inline', position: 'bottom right', equalWeightButtons: true, flipButtons: false }, preferencesModal: { layout: 'box', equalWeightButtons: true, flipButtons: false } }, onChange: ({changedCategories, changedServices}) => { alert(changedCategories); alert(changedServices,changedServices); if(CookieConsent.getUserPreferences().rejectedCategories.indexOf('necessary')<0){ app.setCookie('cookie_consent',1); } }, onModalHide: ({modalName}) => { if(CookieConsent.getUserPreferences().rejectedCategories.indexOf('necessary')<0){ app.setCookie('cookie_consent',1); } }, categories: { necessary: { enabled: true, // this category is enabled by default readOnly: true // this category cannot be disabled }, analytics: { autoClear: { cookies: [ { name: /^_ga/, // regex: match all cookies starting with '_ga' }, { name: '_gid', // string: exact cookie name } ] }, // https://cookieconsent.orestbida.com/reference/configuration-reference.html#category-services services: { ga: { label: 'Google Analytics', onAccept: () => {}, onReject: () => {} }, youtube: { label: 'Youtube Embed', onAccept: () => {}, onReject: () => {} }, } }, //ads: {} }, language: { default: 'en', translations: { en: { consentModal: { title: 'We use cookies', description: 'We use cookies to provide our services and for analytics and marketing. To find out more about our use of cookies, please see our Privacy Policy. By continuing to browse our website, you agree to our use of cookies. <a href="page/cookie-policy">Cookie policy</a>', acceptAllBtn: 'Accept all', acceptNecessaryBtn: 'Accept Necessary', showPreferencesBtn: 'Manage Individual preferences', // closeIconLabel: 'Reject all and close modal', footer: ``, }, preferencesModal: { title: 'Manage cookie preferences', acceptAllBtn: 'Accept all', acceptNecessaryBtn: 'Accept Necessary', savePreferencesBtn: 'Accept current selection', closeIconLabel: 'Close modal', serviceCounterLabel: 'Service|Services', sections: [ { title: 'Your Privacy Choices', description: `In this panel you can express some preferences related to the processing of your personal information. You may review and change expressed choices at any time by resurfacing this panel via the provided link. To deny your consent to the specific processing activities described below, switch the toggles to off or use the “Reject all” button and confirm you want to save your choices.`, }, { title: 'Strictly Necessary', description: 'These cookies are essential for the proper functioning of the website and cannot be disabled.', //this field will generate a toggle linked to the 'necessary' category linkedCategory: 'necessary', cookieTable: { caption: 'Cookie table', headers: { name: 'Cookie', domain: 'Domain', desc: 'Description' }, body: [ { name: 'XSRF-TOKEN', domain: location.hostname, desc: 'csrf security', }, { name: APP_UID+'_session', domain: location.hostname, desc: 'user session', }, { name: APP_UID+'_token', domain: location.hostname, desc: 'user remember', }, { name: APP_UID+'_tz', domain: location.hostname, desc: 'timezone', } ] } }, { title: 'Performance and Analytics', description: 'These cookies collect information about how you use our website. All of the data is anonymized and cannot be used to identify you.', linkedCategory: 'analytics', cookieTable: { caption: 'Cookie table', headers: { name: 'Cookie', domain: 'Domain', desc: 'Description' }, body: [ { name: '_ga', domain: location.hostname, desc: 'Description 1', }, { name: '_gid', domain: location.hostname, desc: 'Description 2', } ] } }, { title: 'More information', description: 'For any queries in relation to my policy on cookies and your choices, please <a href="contact">contact us</a>' } ] } } } } }); }); </script> </body> </html>