package image
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/compute/image"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find images in the specified compartment that match the given pattern.
The search is performed using a fuzzy matching algorithm that searches across multiple fields:
Searchable Fields:
- Name: Image name
- ImageOSVersion: Operating system version of the image
- OperatingSystem: Operating system of the image
- LunchMode: Launch mode of the image
- Tags: All image tags in "key:value" format (e.g., "os_version:8.10")
- TagValues: Just the values of tags without keys (e.g., "8.10")
The search pattern is automatically wrapped with wildcards, so partial matches are supported.
For example, searching for "oracle" will match "oracle-linux", "oracle-vm", etc.
You can also search for specific tag values by using the tag key and value in your search pattern.
For example, "os_version:8.10" will find images with that specific tag.
`
// Examples for the find command
var findExamples = `
# Find images with "oracle" in their name
ocloud compute image find oracle
# Find images with a specific operating system
ocloud compute image find linux
# Find images with a specific tag value
ocloud compute image find os_version:8.10
# Find images with a specific tag value (searching just the value)
ocloud compute image find 8.10
# Find images with "server" in their name and output in JSON format
ocloud compute image find server --json
# Find images with a specific launch mode
ocloud compute image find native
`
// NewFindCmd creates a new command for finding image by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find image by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running image find command", "pattern", namePattern, "in compartment", appCtx.CompartmentName, "json", useJSON)
return image.FindImages(appCtx, namePattern, useJSON)
}
package image
import (
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/compute/image"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all images in the specified compartment with pagination support.
This command displays information about available images in the current compartment.
By default, it shows basic image information such as name, ID, operating system, and launch mode.
The output is paginated, with a default limit of 20 images per page. You can navigate
through pages using the --page flag and control the number of images per page with
the --limit flag.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command shows all available images in the compartment
`
// Examples for the list command
var listExamples = `
# List all images with default pagination (20 per page)
ocloud compute image list
# List images with custom pagination (10 per page, page 2)
ocloud compute image list --limit 10 --page 2
# List images and output in JSON format
ocloud compute image list --json
# List images with custom pagination and JSON output
ocloud compute image list --limit 5 --page 3 --json
`
// NewListCmd creates a new command for listing images
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all images",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add flags specific to the list command
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
logger.LogWithLevel(logger.CmdLogger, 1, "Running image list command in", "compartment", appCtx.CompartmentName, "limit", limit, "page", page, "json", useJSON)
return image.ListImages(appCtx, limit, page, useJSON)
}
package image
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewImageCmd creates a new command for image-related operations
func NewImageCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "image",
Aliases: []string{"img"},
Short: "Manage OCI Image",
Long: "Manage Oracle Cloud Infrastructure Compute Image - list all image or find image by name pattern.",
Example: " ocloud compute image list\n ocloud compute image find <image-name>",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package instance
import (
instaceFlags "github.com/cnopslabs/ocloud/cmd/compute/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/compute/instance"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find instances in the specified compartment that match the given pattern.
The search is performed using a fuzzy matching algorithm that searches across multiple fields:
Searchable Fields:
- Name: Instance name
- ImageName: Name of the image used by the instance
- ImageOperatingSystem: Operating system of the image
- Tags: All instance tags in "key:value" format (e.g., "os_version:8.10")
- TagValues: Just the values of tags without keys (e.g., "8.10")
The search pattern is automatically wrapped with wildcards, so partial matches are supported.
For example, searching for "web" will match "webserver", "web-app", etc.
You can also search for specific tag values by using the tag key and value in your search pattern.
For example, "os_version:8.10" will find instances with that specific tag.
`
// Examples for the find command
var findExamples = `
# Find instances with "web" in their name
ocloud compute instance find web
# Find instances with a specific tag value
ocloud compute instance find os_version:8.10
# Find instances with a specific tag value (searching just the value)
ocloud compute instance find 8.10
# Find instances with "api" in their name and include image details
ocloud compute instance find api --image-details
# Find instances with "server" in their name and output in JSON format
ocloud compute instance find server --json
# Find instances with "oracle" in their image operating system
ocloud compute instance find oracle
`
// NewFindCmd creates a new command for finding instances by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find instances by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
// Add flags specific to the find command
instaceFlags.ImageDetailsFlag.Add(cmd)
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
imageDetails := flags.GetBoolFlag(cmd, flags.FlagNameAllInformation, false)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running instance find command", "pattern", namePattern, "in compartment", appCtx.CompartmentName, "json", useJSON)
return instance.FindInstances(appCtx, namePattern, imageDetails, useJSON)
}
package instance
import (
instaceFlags "github.com/cnopslabs/ocloud/cmd/compute/flags"
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/compute/instance"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all instances in the specified compartment with pagination support.
This command displays information about running instances in the current compartment.
By default, it shows basic instance information such as name, ID, IP address, and shape.
The output is paginated, with a default limit of 20 instances per page. You can navigate
through pages using the --page flag and control the number of instances per page with
the --limit flag.
Additional Information:
- Use --all (-A) to include information about the image used by each instance
- Use --json (-j) to output the results in JSON format
- The command only shows running instances by default
`
// Examples for the list command
var listExamples = `
# List all instances with default pagination (20 per page)
ocloud compute instance list
# List instances with custom pagination (10 per page, page 2)
ocloud compute instance list --limit 10 --page 2
# List instances and include image details
ocloud compute instance list --image-details
# List instances with image details (using shorthand flag)
ocloud compute instance list -i
# List instances and output in JSON format
ocloud compute instance list --json
# List instances with both image details and JSON output
ocloud compute instance list --all --json
`
// NewListCmd creates a new command for listing instances
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all instances",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add flags specific to the list command
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
instaceFlags.ImageDetailsFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
imageDetails := flags.GetBoolFlag(cmd, flags.FlagNameAllInformation, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running instance list command in", "compartment", appCtx.CompartmentName, "limit", limit, "page", page, "json", useJSON, "imageDetails", imageDetails)
return instance.ListInstances(appCtx, limit, page, useJSON, imageDetails)
}
package instance
import (
"github.com/spf13/cobra"
"github.com/cnopslabs/ocloud/internal/app"
)
// NewInstanceCmd creates a new command for instance-related operations
func NewInstanceCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "instance",
Aliases: []string{"inst"},
Short: "Manage OCI Instances",
Long: "Manage Oracle Cloud Infrastructure Compute Instances - list all instances or find instances by name pattern.",
Example: " ocloud compute instance list\n ocloud compute instance find myinstance",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package oke
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/compute/oke"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find Oracle Kubernetes Engine (OKE) clusters in the specified compartment that match the given pattern.
This command searches for OKE clusters whose names or node pool names match the specified pattern.
By default, it shows detailed cluster information such as name, ID, Kubernetes version,
endpoint, and associated node pools for all matching clusters.
The search is performed using fuzzy matching, which means it will find clusters
even if the pattern is only partially matched. The search is case-insensitive.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command searches across all available clusters in the compartment
`
// Examples for the find command
var findExamples = `
# Find clusters with names containing "prod"
ocloud compute oke find prod
# Find clusters with names containing "dev" and output in JSON format
ocloud compute oke find dev --json
# Find clusters with names containing "test" (case-insensitive)
ocloud compute oke find test
`
// NewFindCmd creates a new command for finding OKE clusters by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find OKE clusters by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running oke find command", "pattern", namePattern, "in compartment", appCtx.CompartmentName, "json", useJSON)
return oke.FindClusters(appCtx, namePattern, useJSON)
}
package oke
import (
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/compute/oke"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all Oracle Kubernetes Engine (OKE) clusters in the specified compartment.
This command displays information about all OKE clusters in the current compartment,
including their names, Kubernetes versions, endpoints, and associated node pools.
By default, it shows basic cluster information in a tabular format.
Additional Information:
- Use --json (-j) to output the results in JSON format
- Use --limit (-m) to control the number of results per page
- Use --page (-p) to navigate between pages of results
`
// Examples for the list command
var listExamples = `
# List all OKE clusters in the current compartment
ocloud compute oke list
# List all OKE clusters and output in JSON format
ocloud compute oke list --json
# List OKE clusters with pagination (10 per page, page 2)
ocloud compute oke list --limit 10 --page 2
`
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all Oracle Kubernetes Engine (OKE) clusters",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add pagination flags
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running oke list command in", "compartment", appCtx.CompartmentName, "json", useJSON)
return oke.ListClusters(appCtx, useJSON, limit, page)
}
package oke
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewOKECmd creates a new command for OKE-related operations
func NewOKECmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "oke",
Short: "Manage OCI Kubernetes Engine (OKE)",
Long: "Manage Oracle Cloud Infrastructure Kubernetes Engine (OKE) clusters and node pools.\n\nThis command allows you to list all clusters in a compartment or find specific clusters by name pattern. For each cluster, you can view detailed information including Kubernetes version, endpoint, and associated node pools.",
Example: " ocloud compute oke list\n ocloud compute oke list --json\n ocloud compute oke find myoke\n ocloud compute oke find myoke --json",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package compute
import (
"github.com/cnopslabs/ocloud/cmd/compute/image"
"github.com/cnopslabs/ocloud/cmd/compute/instance"
"github.com/cnopslabs/ocloud/cmd/compute/oke"
"github.com/spf13/cobra"
"github.com/cnopslabs/ocloud/internal/app"
)
// NewComputeCmd creates a new command for compute-related operations
func NewComputeCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "compute",
Aliases: []string{"comp"},
Short: "Manage OCI compute services",
Long: "Manage Oracle Cloud Infrastructure Compute services such as instances, image, and more.",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands, passing in the ApplicationContext
cmd.AddCommand(instance.NewInstanceCmd(appCtx))
cmd.AddCommand(image.NewImageCmd(appCtx))
cmd.AddCommand(oke.NewOKECmd(appCtx))
return cmd
}
package cmd
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/spf13/cobra"
"os"
)
// InitializeAppContext checks for help-related flags and initializes the ApplicationContext accordingly.
// It returns an error instead of exiting directly.
func InitializeAppContext(ctx context.Context, tempRoot *cobra.Command) (*app.ApplicationContext, error) {
// Check if a help flag is present
isHelpRequested := HasHelpFlag(os.Args)
var appCtx *app.ApplicationContext
var err error
if isHelpRequested {
// If help is requested, create a minimal ApplicationContext without cloud configuration
appCtx = &app.ApplicationContext{
Logger: logger.CmdLogger,
CompartmentName: flags.FlagValueHelpMode, // Set a dummy value to avoid nil pointer issues.
}
} else {
// One-shot bootstrap of ApplicationContext
appCtx, err = app.InitApp(ctx, tempRoot)
if err != nil {
return nil, fmt.Errorf("initializing application: %w", err)
}
}
return appCtx, nil
}
// HasHelpFlag checks if any help-related flags are present in the arguments.
func HasHelpFlag(args []string) bool {
for _, arg := range args {
if arg == flags.FlagPrefixShortHelp || arg == flags.FlagPrefixLongHelp || arg == flags.FlagNameHelp {
return true
}
}
return false
}
package autonomousdb
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/database/autonomousdb"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find Autonomous Databases in the specified compartment that match the given pattern.
The search is performed using a fuzzy matching algorithm that searches across multiple fields:
Searchable Fields:
- Name: Database name
- DisplayName: Display name of the database
- DbName: Database name
The search pattern is automatically wrapped with wildcards, so partial matches are supported.
For example, searching for "prod" will match "production", "product-db", etc.
You can also search for specific tag values by using the tag key and value in your search pattern.
For example, "environment:production" will find databases with that specific tag.
`
// Examples for the find command
var findExamples = `
# Find Autonomous Databases with "prod" in their name
ocloud database autonomousdb find prod
# Find Autonomous Databases with "test" in their name and output in JSON format
ocloud database autonomousdb find test --json
`
// NewFindCmd creates a new command for finding compartments by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find Database by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
logger.LogWithLevel(logger.CmdLogger, 1, "Running find command", "pattern", namePattern, "json", useJSON)
return autonomousdb.FindAutonomousDatabases(appCtx, namePattern, useJSON)
}
package autonomousdb
import (
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/services/database/autonomousdb"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all Autonomous Databases in the specified compartment with pagination support.
This command displays information about available Autonomous Databases in the current compartment.
By default, it shows basic database information such as name, ID, state, and workload type.
The output is paginated, with a default limit of 20 databases per page. You can navigate
through pages using the --page flag and control the number of databases per page with
the --limit flag.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command shows all available Autonomous Databases in the compartment
`
// Examples for the list command
var listExamples = `
# List all Autonomous Databases with default pagination (20 per page)
ocloud database autonomousdb list
# List Autonomous Databases with custom pagination (10 per page, page 2)
ocloud database autonomousdb list --limit 10 --page 2
# List Autonomous Databases and output in JSON format
ocloud database autonomousdb list --json
# List Autonomous Databases with custom pagination and JSON output
ocloud database autonomousdb list --limit 5 --page 3 --json
`
// NewListCmd creates a "list" subcommand for listing all databases in the specified compartment with pagination support.
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all Databases in the specified compartment",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add flags specific to the list command
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
return autonomousdb.ListAutonomousDatabase(appCtx, useJSON, limit, page)
}
package autonomousdb
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewAutonomousDatabaseCmd creates a new command for database-related operations
func NewAutonomousDatabaseCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "autonomous",
Aliases: []string{"adb"},
Short: "Manage OCI Compartments",
Long: "Manage Oracle Cloud Infrastructure Databases - list all databases or find database by pattern.",
Example: " ocloud database autonomous list \n ocloud database autonomous find mydatabase",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package database
import (
"github.com/cnopslabs/ocloud/cmd/database/autonomousdb"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewDatabaseCmd creates a new cobra.Command to manage Oracle Cloud Infrastructure database services.
// It provides functionality for managing Autonomous Databases, HeatWave MySQL, and other database types.
func NewDatabaseCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "database",
Aliases: []string{"db"},
Short: "Manage OCI Database services",
Long: "Manage Oracle Cloud Infrastructure database services such as Autonomous Database, HeatWave MySql and more.",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands, passing in the ApplicationContext
cmd.AddCommand(autonomousdb.NewAutonomousDatabaseCmd(appCtx))
return cmd
}
package compartment
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/identity/compartment"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find Compartments in the specified tenancy that match the given pattern.
This command searches for compartments whose names match the specified pattern.
By default, it shows basic compartment information such as name, ID, and description
for all matching compartments.
The search is performed using fuzzy matching, which means it will find compartments
even if the pattern is only partially matched. The search is case-insensitive.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command searches across all available compartments in the tenancy
`
// Examples for the find command
var findExamples = `
# Find compartments with names containing "prod"
ocloud identity compartment find prod
# Find compartments with names containing "dev" and output in JSON format
ocloud identity compartment find dev --json
# Find compartments with names containing "test" (case-insensitive)
ocloud identity compartment find test
`
// NewFindCmd creates a new command for finding compartments by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find compartment by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running find command", "pattern", namePattern, "json", useJSON)
return compartment.FindCompartments(appCtx, namePattern, useJSON)
}
package compartment
import (
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/identity/compartment"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all Compartments in the specified tenancy or compartment with pagination support.
This command displays information about compartments in the current tenancy.
By default, it shows basic compartment information such as name, ID, and description.
The output is paginated, with a default limit of 20 compartments per page. You can navigate
through pages using the --page flag and control the number of compartments per page with
the --limit flag.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command shows all available compartments in the tenancy
`
// Examples for the list command
var listExamples = `
# List all compartments with default pagination (20 per page)
ocloud identity compartment list
# List compartments with custom pagination (10 per page, page 2)
ocloud identity compartment list --limit 10 --page 2
# List compartments and output in JSON format
ocloud identity compartment list --json
# List compartments with custom pagination and JSON output
ocloud identity compartment list --limit 5 --page 3 --json
`
// NewListCmd creates a new Cobra command for listing compartments in a specified tenancy or compartment.
// It supports pagination and optional JSON output.
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all Compartments in the specified tenancy or compartment",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add flags specific to the list command
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running compartment list command in", "compartment", appCtx.CompartmentName, "json", useJSON)
return compartment.ListCompartments(appCtx, useJSON, limit, page)
}
package compartment
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewCompartmentCmd creates a new command for compartment-related operations
func NewCompartmentCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "compartment",
Aliases: []string{"compart"},
Short: "Manage OCI Compartments",
Long: "Manage Oracle Cloud Infrastructure Compartments - list all compartments or find compartment by pattern.",
Example: " ocloud identity compartment list \n ocloud identity compartment find mycompartment",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package policy
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/identity/policy"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find Policies in the specified compartment that match the given pattern.
The search is performed using a fuzzy matching algorithm that searches across multiple fields:
Searchable Fields:
- Name: Policy name
- Description: Description of the policy
- Statement: Policy statements
The search pattern is automatically wrapped with wildcards, so partial matches are supported.
For example, searching for "admin" will match "administrators", "admin-policy", etc.
You can also search for specific tag values by using the tag key and value in your search pattern.
For example, "environment:production" will find policies with that specific tag.
`
// Examples for the find command
var findExamples = `
# Find Policies with "admin" in their name
ocloud identity policy find admin
# Find Policies with "network" in their name and output in JSON format
ocloud identity policy find network --json
`
// NewFindCmd creates a new command for finding policies by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find Policies by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running policy find command", "pattern", namePattern, "json", useJSON)
return policy.FindPolicies(appCtx, namePattern, useJSON)
}
package policy
import (
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/identity/policy"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all Policies in the specified compartment with pagination support.
This command displays information about available Policies in the current compartment.
By default, it shows basic policy information such as name, ID, and description.
The output is paginated, with a default limit of 20 policies per page. You can navigate
through pages using the --page flag and control the number of policies per page with
the --limit flag.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command shows all available Policies in the compartment
`
// Examples for the list command
var listExamples = `
# List all Policies with default pagination (20 per page)
ocloud identity policy list
# List Policies with custom pagination (10 per page, page 2)
ocloud identity policy list --limit 10 --page 2
# List Policies and output in JSON format
ocloud identity policy list --json
# List Policies with custom pagination and JSON output
ocloud identity policy list --limit 5 --page 3 --json
`
// NewListCmd creates a new cobra.Command for listing all policies in a specified tenancy or compartment.
// The command supports pagination through the --limit and --page flags for controlling list size and navigation.
// It also provides optional JSON output for formatted results using the --json flag.
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all Policies in the specified tenancy or compartment",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add flags specific to the list command
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running policy list command in", "compartment", appCtx.CompartmentName, "json", useJSON)
return policy.ListPolicies(appCtx, useJSON, limit, page)
}
package policy
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewPolicyCmd creates a new command for policy-related operations
func NewPolicyCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "policy",
Aliases: []string{"pol"},
Short: "Manage OCI Policies",
Long: "Manage Oracle Cloud Infrastructure Policies - list all policies or find policy by pattern.",
Example: " ocloud identity policy list \n ocloud identity policy find mypolicy",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package identity
import (
"github.com/cnopslabs/ocloud/cmd/identity/compartment"
"github.com/cnopslabs/ocloud/cmd/identity/policy"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewIdentityCmd creates a new cobra.Command for managing OCI identity services such as compartments, polices and bastions.
func NewIdentityCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "identity",
Aliases: []string{"ident", "idt"},
Short: "Manage OCI identity services",
Long: "Manage Oracle Cloud Infrastructure Identity services such as compartments, policies, bastion sessions and more.",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands, passing in the ApplicationContext
cmd.AddCommand(compartment.NewCompartmentCmd(appCtx))
cmd.AddCommand(policy.NewPolicyCmd(appCtx))
return cmd
}
package cmd
import (
"fmt"
"github.com/cnopslabs/ocloud/cmd/version"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/spf13/cobra"
"os"
)
// setLogLevel sets the logging level and colored output based on command-line flags or default values.
// It ensures consistent log settings, initializes the logger, and applies settings globally.
func setLogLevel(tempRoot *cobra.Command) error {
// Check for version flag first - if present, print version and exit
// This is needed because the version flag is added to the real root command,
// but not to the temporary root command used for initial flag parsing.
// By checking for the version flag here, we ensure that both `./ocloud -v`
// and `./ocloud --version` work properly.
for _, arg := range os.Args {
if arg == flags.FlagPrefixVersion || arg == flags.FlagPrefixShortVersion {
version.PrintVersion()
os.Exit(0)
}
}
tempRoot.ParseFlags(os.Args)
// Parse the flags to get the log level Should be approach, but for some reason it prevents parsing flags and give an error
//if err: = tempRoot.ParseFlags(os.Args); err != nil {
// return fmt.Errorf("parsing flags: %w", err)
//}
// Check for a debug flag first - it takes precedence over log-level
debugFlag := tempRoot.PersistentFlags().Lookup(flags.FlagNameDebug)
if debugFlag != nil && debugFlag.Value.String() == flags.FlagValueTrue {
// If a debug flag is set, set the log level to debug
logger.LogLevel = flags.FlagNameDebug
} else {
// Otherwise, use the log-level flag
logLevelFlag := tempRoot.PersistentFlags().Lookup(flags.FlagNameLogLevel)
if logLevelFlag != nil {
// Use the value from the parsed flag
logger.LogLevel = logLevelFlag.Value.String()
if logger.LogLevel == "" {
// If not set, use the default value
logger.LogLevel = flags.FlagValueInfo
}
}
}
// This is a Hack!
// Check if -d or --debug flag is explicitly set in the command line arguments
// This ensures that debug mode is set correctly regardless of whether
// the full command or shorthand flags are used
for _, arg := range os.Args {
if arg == flags.FlagPrefixDebug || arg == flags.FlagPrefixShortDebug {
logger.LogLevel = flags.FlagNameDebug
break
}
}
// Set the colored output from the flag value
colorFlag := tempRoot.PersistentFlags().Lookup(flags.FlagNameColor)
if colorFlag != nil {
// Use the value from the parsed flag
colorValue := colorFlag.Value.String()
logger.ColoredOutput = colorValue == flags.FlagValueTrue
}
// This is a Hack!
// Check if --color flag is explicitly set in the command line arguments
// This ensures that the color setting is set correctly regardless of whether
// the full command or shorthand flags are used
for _, arg := range os.Args {
if arg == flags.FlagPrefixColor {
logger.ColoredOutput = true
break
}
}
// Initialize logger
if err := logger.SetLogger(); err != nil {
return fmt.Errorf("initializing logger: %w", err)
}
// Initialize package-level logger with the same logger instance
logger.InitLogger(logger.CmdLogger)
return nil
}
package network
import (
"github.com/cnopslabs/ocloud/cmd/network/subnet"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewNetworkCmd creates a new cobra.Command for managing OCI network services such as vcn, subnets, load balancers and more
func NewNetworkCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "network",
Aliases: []string{"net"},
Short: "Manage OCI networking services",
Long: "Manage Oracle Cloud Infrastructure Networking services such as vcn, subnets and more.",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands, passing in the ApplicationContext
cmd.AddCommand(subnet.NewSubnetCmd(appCtx))
return cmd
}
package subnet
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/network/subnet"
"github.com/spf13/cobra"
)
// Long description for the find command
var findLong = `
Find Subnets in the specified tenancy or compartment that match the given pattern.
This command searches for subnets whose names match the specified pattern.
By default, it shows detailed subnet information such as name, ID, CIDR block,
and whether public IP addresses are allowed for all matching subnets.
The search is performed using fuzzy matching, which means it will find subnets
even if the pattern is only partially matched. The search is case-insensitive.
Additional Information:
- Use --json (-j) to output the results in JSON format
- The command searches across all available subnets in the compartment
`
// Examples for the find command
var findExamples = `
# Find subnets with names containing "prod"
ocloud network subnet find prod
# Find subnets with names containing "dev" and output in JSON format
ocloud network subnet find dev --json
# Find subnets with names containing "test" (case-insensitive)
ocloud network subnet find test
`
// NewFindCmd creates a new command for finding subnets by name pattern
func NewFindCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "find [pattern]",
Aliases: []string{"f"},
Short: "Find Subnets by name pattern",
Long: findLong,
Example: findExamples,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunFindCommand(cmd, args, appCtx)
},
}
return cmd
}
// RunFindCommand handles the execution of the find command
func RunFindCommand(cmd *cobra.Command, args []string, appCtx *app.ApplicationContext) error {
namePattern := args[0]
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running subnet find command", "pattern", namePattern, "json", useJSON)
return subnet.FindSubnets(appCtx, namePattern, useJSON)
}
package subnet
import (
paginationFlags "github.com/cnopslabs/ocloud/cmd/flags"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/network/subnet"
"github.com/spf13/cobra"
)
// Long description for the list command
var listLong = `
List all Subnets in the specified tenancy or compartment.
This command displays information about all subnets in the current compartment,
including their names, CIDR blocks, and whether they allow public IP addresses.
By default, it shows basic subnet information in a tabular format.
Additional Information:
- Use --json (-j) to output the results in JSON format
- Use --limit (-m) to control the number of results per page
- Use --page (-p) to navigate between pages of results
- Use --sort (-s) to sort results by name or CIDR
`
// Examples for the list command
var listExamples = `
# List all subnets in the current compartment
ocloud network subnet list
# List all subnets and output in JSON format
ocloud network subnet list --json
# List subnets with pagination (10 per page, page 2)
ocloud network subnet list --limit 10 --page 2
# List subnets sorted by name
ocloud network subnet list --sort name
# List subnets sorted by CIDR block
ocloud network subnet list --sort cidr
`
// NewListCmd creates a new "list" command for listing all subnets in a specified tenancy or compartment.
// Accepts an ApplicationContext for accessing configuration and dependencies.
// Adds pagination flags for controlling the number of results returned and the page to retrieve.
// Executes the RunListCommand function when invoked.
func NewListCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"l"},
Short: "List all Subnets in the specified tenancy or compartment",
Long: listLong,
Example: listExamples,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
return RunListCommand(cmd, appCtx)
},
}
// Add flags specific to the list command
paginationFlags.LimitFlag.Add(cmd)
paginationFlags.PageFlag.Add(cmd)
paginationFlags.SortFlag.Add(cmd)
return cmd
}
// RunListCommand handles the execution of the list command
func RunListCommand(cmd *cobra.Command, appCtx *app.ApplicationContext) error {
// Get pagination parameters
limit := flags.GetIntFlag(cmd, flags.FlagNameLimit, paginationFlags.FlagDefaultLimit)
page := flags.GetIntFlag(cmd, flags.FlagNamePage, paginationFlags.FlagDefaultPage)
useJSON := flags.GetBoolFlag(cmd, flags.FlagNameJSON, false)
sortBy := flags.GetStringFlag(cmd, flags.FlagNameSort, "")
// Use LogWithLevel to ensure debug logs work with shorthand flags
logger.LogWithLevel(logger.CmdLogger, 1, "Running subnet list command in", "compartment", appCtx.CompartmentName, "json", useJSON, "sort", sortBy)
return subnet.ListSubnets(appCtx, useJSON, limit, page, sortBy)
}
package subnet
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/spf13/cobra"
)
// NewSubnetCmd creates a new command for subnet-related operations
func NewSubnetCmd(appCtx *app.ApplicationContext) *cobra.Command {
cmd := &cobra.Command{
Use: "subnet",
Aliases: []string{"sub"},
Short: "Manage OCI Subnets",
Long: "Manage Oracle Cloud Infrastructure Subnets - list all subnets or find subnet by pattern.",
Example: " ocloud network subnet list \n ocloud network subnet find mysubnet",
SilenceUsage: true,
SilenceErrors: true,
}
// Add subcommands
cmd.AddCommand(NewListCmd(appCtx))
cmd.AddCommand(NewFindCmd(appCtx))
return cmd
}
package cmd
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/cmd/compute"
"github.com/cnopslabs/ocloud/cmd/database"
"github.com/cnopslabs/ocloud/cmd/identity"
"github.com/cnopslabs/ocloud/cmd/network"
"github.com/cnopslabs/ocloud/cmd/version"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/spf13/cobra"
)
// NewRootCmd creates a new root command with all subcommands attached
func NewRootCmd(appCtx *app.ApplicationContext) *cobra.Command {
rootCmd := &cobra.Command{
Use: "ocloud",
Short: "Interact with Oracle Cloud Infrastructure",
Long: "",
SilenceUsage: true,
}
// Initialize global flags
flags.AddGlobalFlags(rootCmd)
// Add version command
rootCmd.AddCommand(version.NewVersionCommand(appCtx))
version.AddVersionFlag(rootCmd)
rootCmd.AddCommand(compute.NewComputeCmd(appCtx))
rootCmd.AddCommand(identity.NewIdentityCmd(appCtx))
rootCmd.AddCommand(database.NewDatabaseCmd(appCtx))
rootCmd.AddCommand(network.NewNetworkCmd(appCtx))
return rootCmd
}
// Execute runs the root command with the given context.
// It now returns an error instead of exiting directly.
func Execute(ctx context.Context) error {
// Create a temporary root command for bootstrapping
tempRoot := &cobra.Command{
Use: "ocloud",
Short: "Interact with Oracle Cloud Infrastructure",
Long: "",
SilenceUsage: true,
}
flags.AddGlobalFlags(tempRoot)
if err := setLogLevel(tempRoot); err != nil {
return fmt.Errorf("setting log level: %w", err)
}
appCtx, err := InitializeAppContext(ctx, tempRoot)
if err != nil {
return fmt.Errorf("initializing app context: %w", err)
}
// Create the real root command with the ApplicationContext
root := NewRootCmd(appCtx)
// Switch to RunE for the root command
root.RunE = func(cmd *cobra.Command, args []string) error {
return cmd.Help() // The default behavior is to show help
}
// Execute the command
if err := root.ExecuteContext(ctx); err != nil {
return fmt.Errorf("failed to execute root command: %w", err)
}
return nil
}
package version
import (
"fmt"
"github.com/cnopslabs/ocloud/buildinfo"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/spf13/cobra"
"io"
"os"
)
// VersionInfo encapsulates the version command functionality
// It wraps a cobra.Command and provides methods to handle version information display
type VersionInfo struct {
cmd *cobra.Command
writer io.Writer
}
// NewVersionCommand creates and configures a new version command
// Returns a *cobra.Command that can be added to the root command
// This function was refactored to return *cobra.Command directly instead of *VersionInfo
// to fix an issue with adding the command to the root command
func NewVersionCommand(appCtx *app.ApplicationContext) *cobra.Command {
// If appCtx is nil, use os.Stdout as the default writer
var writer io.Writer = os.Stdout
if appCtx != nil {
writer = appCtx.Stdout
}
vc := &VersionInfo{
writer: writer,
}
vc.cmd = &cobra.Command{
Use: "version",
Short: "Print the version information",
Long: "Print the version, build time, and git commit hash information",
RunE: vc.runCommand,
}
return vc.cmd
}
// runCommand handles the main command execution
func (vc *VersionInfo) runCommand(cmd *cobra.Command, args []string) error {
return vc.printVersionInfo()
}
// printVersionInfo displays the version information
func (vc *VersionInfo) printVersionInfo() error {
PrintVersionInfo(vc.writer)
return nil
}
// PrintVersionInfo prints complete version information to the specified writer
// This function was updated to print all version information (version, commit hash, and build time)
// to ensure consistency between the version command and the version flag
func PrintVersionInfo(w io.Writer) {
fmt.Fprintf(w, "Version: %s\n", buildinfo.Version)
fmt.Fprintf(w, "Commit: %s\n", buildinfo.CommitHash)
fmt.Fprintf(w, "Built: %s\n", buildinfo.BuildTime)
}
// PrintVersion prints version information to stdout
// This function is used by the root command when the --version flag is specified
// It was added to fix an issue where cmd/root.go was calling version.PrintVersion()
// which didn't exist in the version package
func PrintVersion() {
PrintVersionInfo(os.Stdout)
}
// AddVersionFlag adds a version flag to the root command
// This function adds a global persistent flag to support the --version/-v flag
// and sets up a PersistentPreRunE hook to check for the flag and print version information
// Note: This function preserves any existing PersistentPreRunE hook by storing it and
// calling it after checking for the version flag
func AddVersionFlag(rootCmd *cobra.Command) {
// Register a global persistent flag to support short form (e.g., `ocloud -v`)
rootCmd.PersistentFlags().BoolP(flags.FlagNameVersion, flags.FlagShortVersion, false, flags.FlagDescVersion)
// Store the original PersistentPreRunE function
originalPreRun := rootCmd.PersistentPreRunE
// Override the persistent pre-run hook to check for the `-v` flag
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if versionFlag := flags.GetBoolFlag(cmd, flags.FlagNameVersion, false); versionFlag {
PrintVersionInfo(os.Stdout)
return nil
}
// Call the original PersistentPreRunE if it exists
if originalPreRun != nil {
return originalPreRun(cmd, args)
}
return nil
}
}
package app
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/oci"
"io"
"os"
"github.com/go-logr/logr"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/identity"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cnopslabs/ocloud/internal/config"
"github.com/cnopslabs/ocloud/internal/config/flags"
"github.com/cnopslabs/ocloud/internal/logger"
)
// ApplicationContext represents the application with all its clients, configuration, and resolved IDs.
// It holds all the components needed for command execution.
type ApplicationContext struct {
Provider common.ConfigurationProvider
IdentityClient identity.IdentityClient
TenancyID string
TenancyName string
CompartmentName string
CompartmentID string
Logger logr.Logger
EnableConcurrency bool
Stdout io.Writer
Stderr io.Writer
}
// InitApp initializes the application context, setting up configuration, clients, logging, and determineConcurrencyStatus settings.
// Returns an ApplicationContext instance and an error if initialization fails.
func InitApp(ctx context.Context, cmd *cobra.Command) (*ApplicationContext, error) {
logger.CmdLogger.Info("Initializing application")
provider := config.LoadOCIConfig()
identityClient, err := oci.NewIdentityClient(provider)
if err != nil {
return nil, err
}
configureClientRegion(identityClient)
enableConcurrency := determineConcurrencyStatus(cmd)
appCtx := &ApplicationContext{
Provider: provider,
IdentityClient: identityClient,
CompartmentName: viper.GetString(flags.FlagNameCompartment),
Logger: logger.CmdLogger,
EnableConcurrency: enableConcurrency,
}
// Set the standard writers for the application's lifetime.
appCtx.Stdout = os.Stdout
appCtx.Stderr = os.Stderr
if err := resolveTenancyAndCompartment(ctx, cmd, appCtx); err != nil {
return nil, fmt.Errorf("resolving tenancy and compartment: %w", err)
}
return appCtx, nil
}
// configureClientRegion checks the `OCI_REGION` environment variable and overrides the client's region if it is set.
func configureClientRegion(client identity.IdentityClient) {
if region, ok := os.LookupEnv(flags.EnvOCIRegion); ok {
client.SetRegion(region)
logger.LogWithLevel(logger.CmdLogger, 3, "overriding region from env", "region", region)
}
}
// determineConcurrencyStatus determines whether determineConcurrencyStatus is enabled based on command flags and specific CLI arguments.
// Returns true if determineConcurrencyStatus is enabled, or false if explicitly disabled via flags or defaults to enabled.
func determineConcurrencyStatus(cmd *cobra.Command) bool {
disable := flags.GetBoolFlag(cmd, flags.FlagNameDisableConcurrency, false)
explicit := cmd.Flags().Changed(flags.FlagNameDisableConcurrency)
if explicit {
return !disable // Invert the value since the flag is "disable-determineConcurrencyStatus"
}
for _, arg := range os.Args {
if arg == flags.FlagPrefixShortDisableConcurrency || arg == flags.FlagPrefixDisableConcurrency {
return false
}
}
return true // default to enabled
}
// resolveTenancyAndCompartment resolves the tenancy ID, tenancy name, and compartment ID for the application context.
// It uses various sources such as CLI flags, environment variables, mapping files, and OCI configuration.
// Updates the provided ApplicationContext with the resolved IDs and names. Returns an error if resolution fails.
func resolveTenancyAndCompartment(ctx context.Context, cmd *cobra.Command, appCtx *ApplicationContext) error {
tenancyID, err := resolveTenancyID(cmd)
if err != nil {
return fmt.Errorf("could not resolve tenancy ID: %w", err)
}
appCtx.TenancyID = tenancyID
if name := resolveTenancyName(cmd, appCtx.TenancyID); name != "" {
appCtx.TenancyName = name
}
compID, err := resolveCompartmentID(ctx, appCtx)
if err != nil {
return fmt.Errorf("could not resolve compartment ID: %w", err)
}
appCtx.CompartmentID = compID
return nil
}
// resolveTenancyID resolves the tenancy OCID from various sources in order of precedence:
// 1. Command line flag
// 2. Environment variable
// 3. Tenancy name lookup (if tenancy name is provided)
// 4. OCI config file
// Returns the tenancy ID or an error if it cannot be resolved.
func resolveTenancyID(cmd *cobra.Command) (string, error) {
// Check if tenancy ID is provided as a flag
if cmd.Flags().Changed(flags.FlagNameTenancyID) {
tenancyID := viper.GetString(flags.FlagNameTenancyID)
logger.LogWithLevel(logger.CmdLogger, 3, "using tenancy OCID from flag", "tenancyID", tenancyID)
return tenancyID, nil
}
// Check if tenancy ID is provided as an environment variable
if envTenancy := os.Getenv(flags.EnvOCITenancy); envTenancy != "" {
logger.LogWithLevel(logger.CmdLogger, 3, "using tenancy OCID from env", "tenancyID", envTenancy)
viper.Set(flags.FlagNameTenancyID, envTenancy)
return envTenancy, nil
}
// Check if the tenancy name is provided as an environment variable
if envTenancyName := os.Getenv(flags.EnvOCITenancyName); envTenancyName != "" {
lookupID, err := config.LookupTenancyID(envTenancyName)
if err != nil {
// Log the error but continue with the next method of resolving the tenancy ID
logger.LogWithLevel(logger.CmdLogger, 3, "could not look up tenancy ID for tenancy name, continuing with other methods", "tenancyName", envTenancyName, "error", err)
// Add a more detailed message about how to set up the mapping file
logger.LogWithLevel(logger.CmdLogger, 3, "To set up tenancy mapping, create a YAML file at ~/.oci/tenancy-map.yaml or set the OCI_TENANCY_MAP_PATH environment variable. The file should contain entries mapping tenancy names to OCIDs. Example:\n- environment: prod\n tenancy: mytenancy\n tenancy_id: ocid1.tenancy.oc1..aaaaaaaabcdefghijklmnopqrstuvwxyz\n realm: oc1\n compartments: mycompartment\n regions: us-ashburn-1")
} else {
logger.LogWithLevel(logger.CmdLogger, 3, "using tenancy OCID for name", "tenancyName", envTenancyName, "tenancyID", lookupID)
viper.Set(flags.FlagNameTenancyID, lookupID)
return lookupID, nil
}
}
// Load from an OCI config file as a last resort
tenancyID, err := config.GetTenancyOCID()
if err != nil {
return "", fmt.Errorf("could not load tenancy OCID: %w", err)
}
logger.LogWithLevel(logger.CmdLogger, 3, "using tenancy OCID from config file", "tenancyID", tenancyID)
viper.Set(flags.FlagNameTenancyID, tenancyID)
return tenancyID, nil
}
// resolveTenancyName resolves the tenancy name from various sources in order of precedence:
// 1. Command line flag
// 2. Environment variable
// 3. Tenancy mapping file lookup (using tenancy ID)
// Returns the tenancy name or an empty string if it cannot be resolved.
func resolveTenancyName(cmd *cobra.Command, tenancyID string) string {
// Check if the tenancy name is provided as a flag
if cmd.Flags().Changed(flags.FlagNameTenancyName) {
tenancyName := viper.GetString(flags.FlagNameTenancyName)
logger.LogWithLevel(logger.CmdLogger, 3, "using tenancy name from flag", "tenancyName", tenancyName)
return tenancyName
}
// Check if the tenancy name is provided as an environment variable
if envTenancyName := os.Getenv(flags.EnvOCITenancyName); envTenancyName != "" {
logger.LogWithLevel(logger.CmdLogger, 3, "using tenancy name from env", "tenancyName", envTenancyName)
viper.Set(flags.FlagNameTenancyName, envTenancyName)
return envTenancyName
}
// Try to find a tenancy name from a mapping file if available
tenancies, err := config.LoadTenancyMap()
if err == nil {
for _, env := range tenancies {
if env.TenancyID == tenancyID {
logger.LogWithLevel(logger.CmdLogger, 3, "found tenancy name from mapping file", "tenancyName", env.Tenancy)
viper.Set(flags.FlagNameTenancyName, env.Tenancy)
return env.Tenancy
}
}
}
return ""
}
// resolveCompartmentID returns the OCID of the compartment whose name matches
// `compartmentName` under the given tenancy. It searches all active compartments
// in the tenancy subtree.
func resolveCompartmentID(ctx context.Context, appCtx *ApplicationContext) (string, error) {
compartmentName := appCtx.CompartmentName
idClient := appCtx.IdentityClient
tenancyOCID := appCtx.TenancyID
// If the compartment name is not set, use tenancy ID as fallback
if compartmentName == "" {
logger.LogWithLevel(logger.CmdLogger, 3, "compartment name not set, using tenancy ID as fallback", "tenancyID", tenancyOCID)
return tenancyOCID, nil
}
// prepare the base request
req := identity.ListCompartmentsRequest{
CompartmentId: &tenancyOCID,
AccessLevel: identity.ListCompartmentsAccessLevelAccessible,
LifecycleState: identity.CompartmentLifecycleStateActive,
CompartmentIdInSubtree: common.Bool(true),
}
// paginate through results; stop when OpcNextPage is nil
pageToken := ""
for {
if pageToken != "" {
req.Page = common.String(pageToken)
}
resp, err := idClient.ListCompartments(ctx, req)
if err != nil {
return "", fmt.Errorf("listing compartments: %w", err)
}
// scan each compartment summary for a name match
for _, comp := range resp.Items {
if comp.Name != nil && *comp.Name == compartmentName {
return *comp.Id, nil
}
}
// if there's no next page, we're done searching
if resp.OpcNextPage == nil {
break
}
pageToken = *resp.OpcNextPage
}
return "", fmt.Errorf("compartment %q not found under tenancy %s", compartmentName, tenancyOCID)
}
// Package flags define flag types and domain-specific flag collections for the CLI.
package flags
import (
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// Global flags
var (
LogLevelFlag = StringFlag{
Name: FlagNameLogLevel,
Default: FlagValueInfo,
Usage: FlagDescLogLevel,
}
DebugFlag = BoolFlag{
Name: FlagNameDebug,
Shorthand: FlagShortDebug,
Default: false,
Usage: FlagDescDebug,
}
ColorFlag = BoolFlag{
Name: FlagNameColor,
Default: false,
Usage: logger.ColoredOutputMsg,
}
TenancyIDFlag = StringFlag{
Name: FlagNameTenancyID,
Shorthand: FlagShortTenancyID,
Default: "",
Usage: FlagDescTenancyID,
}
TenancyNameFlag = StringFlag{
Name: FlagNameTenancyName,
Default: "",
Usage: FlagDescTenancyName,
}
CompartmentFlag = StringFlag{
Name: FlagNameCompartment,
Shorthand: FlagShortCompartment,
Default: "",
Usage: FlagDescCompartment,
}
DisableConcurrencyFlag = BoolFlag{
Name: FlagNameDisableConcurrency,
Shorthand: FlagShortDisableConcurrency,
Default: false,
Usage: FlagDescDisableConcurrency,
}
HelpFlag = BoolFlag{
Name: FlagNameHelp,
Shorthand: FlagShortHelp,
Default: false,
Usage: FlagDescHelp,
}
JSONFlag = BoolFlag{
Name: FlagNameJSON,
Shorthand: FlagShortJSON,
Default: false,
Usage: FlagDescJSON,
}
)
// globalFlags is a slice of all global flags for batch registration
var globalFlags = []Flag{
LogLevelFlag,
DebugFlag,
ColorFlag,
TenancyIDFlag,
TenancyNameFlag,
CompartmentFlag,
DisableConcurrencyFlag,
HelpFlag,
JSONFlag,
}
// AddGlobalFlags adds all global flags to the given command
func AddGlobalFlags(cmd *cobra.Command) {
// Add global flags as persistent flags
for _, f := range globalFlags {
f.Apply(cmd.PersistentFlags())
}
// Set annotation for a help flag
_ = cmd.PersistentFlags().SetAnnotation(FlagNameHelp, CobraAnnotationKey, []string{FlagValueTrue})
// Bind flags to viper for configuration
_ = viper.BindPFlag(FlagNameTenancyID, cmd.PersistentFlags().Lookup(FlagNameTenancyID))
_ = viper.BindPFlag(FlagNameTenancyName, cmd.PersistentFlags().Lookup(FlagNameTenancyName))
_ = viper.BindPFlag(FlagNameCompartment, cmd.PersistentFlags().Lookup(FlagNameCompartment))
// allow ENV overrides, e.g., OCI_CLI_TENANCY, OCI_TENANCY_NAME, OCI_COMPARTMENT
viper.SetEnvPrefix("OCI")
viper.AutomaticEnv()
}
// Package flags provides a type-safe and reusable way to define and manage command-line flags
// for CLI applications using cobra and pflag libraries. It offers structured flag types for
// boolean, string, and integer values, along with consistent interfaces for adding these flags
// to commands and flag sets.
package flags
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// BoolFlag represents a boolean command flag configuration with a name, optional shorthand,
// default value, and usage description. It implements the Flag interface for boolean flags.
type BoolFlag struct {
Name string
Shorthand string
Default bool
Usage string
}
// Add adds the boolean flag to the command
func (f BoolFlag) Add(cmd *cobra.Command) {
cmd.Flags().BoolP(f.Name, f.Shorthand, f.Default, f.Usage)
}
// Apply adds the boolean flag to the given flag set
func (f BoolFlag) Apply(flags *pflag.FlagSet) {
flags.BoolP(f.Name, f.Shorthand, f.Default, f.Usage)
}
// StringFlag represents a string command flag configuration with a name, optional shorthand,
// default value, and usage description. It implements the Flag interface for string flags.
type StringFlag struct {
Name string
Shorthand string
Default string
Usage string
}
// Add adds the string flag to the command
func (f StringFlag) Add(cmd *cobra.Command) {
cmd.Flags().StringP(f.Name, f.Shorthand, f.Default, f.Usage)
}
// Apply adds the string flag to the given flag set
func (f StringFlag) Apply(flags *pflag.FlagSet) {
flags.StringP(f.Name, f.Shorthand, f.Default, f.Usage)
}
// IntFlag represents an integer command flag configuration with a name, optional shorthand,
// default value, and usage description. It implements the Flag interface for integer flags.
type IntFlag struct {
Name string
Shorthand string
Default int
Usage string
}
// Add adds the integer flag to the command
func (f IntFlag) Add(cmd *cobra.Command) {
cmd.Flags().IntP(f.Name, f.Shorthand, f.Default, f.Usage)
}
// Apply adds the integer flag to the given flag set
func (f IntFlag) Apply(flags *pflag.FlagSet) {
flags.IntP(f.Name, f.Shorthand, f.Default, f.Usage)
}
// Flag defines the interface that all flag types must implement to be used within the CLI.
// It provides methods for adding flags to both cobra.Command and pflag.FlagSet, allowing
// flexible flag registration across different command contexts.
type Flag interface {
// Add registers the flag with the provided cobra.Command
Add(*cobra.Command)
// Apply registers the flag with the provided pflag.FlagSet
Apply(*pflag.FlagSet)
}
// Package flags defines flag types and domain-specific flag collections for the CLI.
// It provides utility functions for safely retrieving flag values from cobra commands
// with default fallbacks. This package is used throughout the application to handle
// command-line flags in a consistent manner.
//
// Usage:
// - Import this package in your command handlers
// - Use the Get*Flag functions to safely retrieve flag values
// - Each function handles potential errors and returns a default value if the flag is not found
//
// Example:
// debug := flags.GetBoolFlag(cmd, "debug", false)
// name := flags.GetStringFlag(cmd, "name", "default-name")
package flags
import (
"github.com/spf13/cobra"
)
// GetBoolFlag retrieves a boolean flag value from the cobra.Command.
// It provides a safe way to access boolean flags with automatic error handling.
// If the flag is not found or there's an error reading it, it returns the provided default value.
//
// Parameters:
// - cmd: The cobra.Command instance containing the flags
// - flagName: The name of the flag to retrieve
// - defaultValue: The value to return if the flag is not found or has an error
//
// Returns:
// - The boolean value of the flag or the default value if not found/error
func GetBoolFlag(cmd *cobra.Command, flagName string, defaultValue bool) bool {
value, err := cmd.Flags().GetBool(flagName)
if err != nil {
return defaultValue
}
return value
}
// GetStringFlag gets a string flag value from the command
// If the flag is not found or there's an error, it returns the default value
func GetStringFlag(cmd *cobra.Command, flagName string, defaultValue string) string {
value, err := cmd.Flags().GetString(flagName)
if err != nil {
return defaultValue
}
return value
}
// GetIntFlag gets an integer flag value from the command
// If the flag is not found or there's an error, it returns the default value
func GetIntFlag(cmd *cobra.Command, flagName string, defaultValue int) int {
value, err := cmd.Flags().GetInt(flagName)
if err != nil {
return defaultValue
}
return value
}
// GetStringSliceFlag gets a string slice flag value from the command
// If the flag is not found or there's an error, it returns the default value
func GetStringSliceFlag(cmd *cobra.Command, flagName string, defaultValue []string) []string {
value, err := cmd.Flags().GetStringSlice(flagName)
if err != nil {
return defaultValue
}
return value
}
// GetFloat64Flag gets a float64 flag value from the command
// If the flag is not found or there's an error, it returns the default value
func GetFloat64Flag(cmd *cobra.Command, flagName string, defaultValue float64) float64 {
value, err := cmd.Flags().GetFloat64(flagName)
if err != nil {
return defaultValue
}
return value
}
// GetDurationFlag gets a duration flag value from the command
// If the flag is not found or there's an error, it returns the default value
func GetDurationFlag(cmd *cobra.Command, flagName string, defaultValue int64) int64 {
value, err := cmd.Flags().GetInt64(flagName)
if err != nil {
return defaultValue
}
return value
}
// Package generator provides tools to generate flag-related code and documentation.
package generator
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
)
// FlagInfo represents information about a flag.
type FlagInfo struct {
Name string
Shorthand string
Description string
Default string
}
// GenerateReadmeTable generates a markdown table of flags for the README.
func GenerateReadmeTable(flagInfos []FlagInfo, title string) string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("### %s\n\n", title))
buf.WriteString("| Flag | Short | Description |\n")
buf.WriteString("|------|-------|-------------|\n")
for _, flag := range flagInfos {
shorthand := flag.Shorthand
if shorthand != "" {
shorthand = fmt.Sprintf("`-%s`", shorthand)
}
buf.WriteString(fmt.Sprintf("| `--%s` | %s | %s |\n", flag.Name, shorthand, flag.Description))
}
return buf.String()
}
// ExtractFlagConstants extracts flag constants from the config package.
func ExtractFlagConstants(configDir string) (map[string][]FlagInfo, error) {
// Parse the flags/constants.go file
fset := token.NewFileSet()
flagsFile := filepath.Join(configDir, "flags", "constants.go")
node, err := parser.ParseFile(fset, flagsFile, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("failed to parse flags/constants.go: %w", err)
}
// Extract flag names, shorthands, and descriptions
nameMap := make(map[string]string)
shorthandMap := make(map[string]string)
descMap := make(map[string]string)
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.CONST {
continue
}
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, name := range valueSpec.Names {
if i >= len(valueSpec.Values) {
continue
}
basicLit, ok := valueSpec.Values[i].(*ast.BasicLit)
if !ok || basicLit.Kind != token.STRING {
continue
}
value := strings.Trim(basicLit.Value, "\"")
if strings.HasPrefix(name.Name, "FlagName") {
nameMap[strings.TrimPrefix(name.Name, "FlagName")] = value
} else if strings.HasPrefix(name.Name, "FlagShort") {
shorthandMap[strings.TrimPrefix(name.Name, "FlagShort")] = value
} else if strings.HasPrefix(name.Name, "FlagDesc") {
descMap[strings.TrimPrefix(name.Name, "FlagDesc")] = value
}
}
}
}
// Group flags by category
globalFlags := []FlagInfo{}
instanceFlags := []FlagInfo{}
// Add global flags
for key, name := range nameMap {
if key == "LogLevel" || key == "TenancyID" || key == "TenancyName" || key == "Compartment" {
shorthand := shorthandMap[key]
desc := descMap[key]
globalFlags = append(globalFlags, FlagInfo{
Name: name,
Shorthand: shorthand,
Description: desc,
})
}
}
// Add instance flags
for key, name := range nameMap {
if key == "List" || key == "Find" || key == "ImageDetails" {
shorthand := shorthandMap[key]
desc := descMap[key]
instanceFlags = append(instanceFlags, FlagInfo{
Name: name,
Shorthand: shorthand,
Description: desc,
})
}
}
result := make(map[string][]FlagInfo)
result["global"] = globalFlags
result["instance"] = instanceFlags
return result, nil
}
// UpdateReadme updates the README.md file with generated flag tables.
func UpdateReadme(readmePath string, flagInfos map[string][]FlagInfo) error {
// Read the README file
content, err := os.ReadFile(readmePath)
if err != nil {
return fmt.Errorf("failed to read README.md: %w", err)
}
// Generate flag tables
globalTable := GenerateReadmeTable(flagInfos["global"], "Global Flags")
instanceTable := GenerateReadmeTable(flagInfos["instance"], "Instance Command Flags")
// Define regex patterns to find and replace the flag tables
globalPattern := regexp.MustCompile(`(?s)### Global Flags\n\n\|.*?\n\|.*?\n(?:\|.*?\n)+`)
instancePattern := regexp.MustCompile(`(?s)### Instance Command Flags\n\n\|.*?\n\|.*?\n(?:\|.*?\n)+`)
// Replace the flag tables
newContent := globalPattern.ReplaceAllString(string(content), globalTable)
newContent = instancePattern.ReplaceAllString(newContent, instanceTable)
// Write the updated content back to the README file
return os.WriteFile(readmePath, []byte(newContent), 0644)
}
// Command main is the entry point for the flag generator.
// It extracts flag constants from the config package and updates the README.md file.
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/cnopslabs/ocloud/internal/config/generator"
)
func main() {
// Get the project root directory
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting working directory: %v\n", err)
os.Exit(1)
}
// Find the project root (where go.mod is located)
projectRoot := findProjectRoot(wd)
if projectRoot == "" {
fmt.Fprintf(os.Stderr, "Error: could not find project root (go.mod file)\n")
os.Exit(1)
}
// Extract flag constants
configDir := filepath.Join(projectRoot, "internal", "config")
flagInfos, err := generator.ExtractFlagConstants(configDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error extracting flag constants: %v\n", err)
os.Exit(1)
}
// Update README.md
readmePath := filepath.Join(projectRoot, "README.md")
err = generator.UpdateReadme(readmePath, flagInfos)
if err != nil {
fmt.Fprintf(os.Stderr, "Error updating README.md: %v\n", err)
os.Exit(1)
}
fmt.Println("Successfully updated README.md with flag documentation")
}
// findProjectRoot finds the project root directory by looking for a go.mod file.
func findProjectRoot(dir string) string {
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
return ""
}
dir = parent
}
}
package config
import (
"fmt"
"github.com/cnopslabs/ocloud/internal/logger"
"gopkg.in/yaml.v3"
"path/filepath"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/pkg/errors"
"os"
)
// For testing purposes
var (
// MockGetTenancyOCID allows tests to override the GetTenancyOCID function
MockGetTenancyOCID func() (string, error)
// MockLookupTenancyID allows tests to override the LookupTenancyID function
MockLookupTenancyID func(tenancyName string) (string, error)
)
// DefaultTenancyMapPath defines the default file path for the OCI tenancy map configuration in the user's home directory.
// If the home directory cannot be determined, it falls back to an empty string.
var DefaultTenancyMapPath = func() string {
dir, err := getUserHomeDir()
if err != nil {
logger.LogWithLevel(logger.Logger, 1, "failed to get user home directory for tenancy map path", "error", err)
return ""
}
return filepath.Join(dir, ".oci", "tenancy-map.yaml")
}()
const (
defaultProfile = "DEFAULT"
envProfileKey = "OCI_CLI_PROFILE"
configDir = ".oci"
configFile = "config"
// EnvTenancyMapPath is the environment variable key used to specify the file path for the OCI tenancy map configuration.
EnvTenancyMapPath = "OCI_TENANCY_MAP_PATH"
)
// LoadOCIConfig picks the profile from env or default, and logs at debug level.
// If there's an error getting the home directory, it falls back to the default provider.
func LoadOCIConfig() common.ConfigurationProvider {
profile := GetOCIProfile()
if profile == defaultProfile {
logger.LogWithLevel(logger.Logger, 3, "using default profile")
return common.DefaultConfigProvider()
}
logger.LogWithLevel(logger.Logger, 3, "using profile", "profile", profile)
homeDir, err := getUserHomeDir()
if err != nil {
logger.Logger.Error(err, "failed to get user home directory for config path, falling back to default provider")
return common.DefaultConfigProvider()
}
path := filepath.Join(homeDir, configDir, configFile)
return common.CustomProfileConfigProvider(path, profile)
}
// GetOCIProfile returns OCI_CLI_PROFILE or "DEFAULT".
func GetOCIProfile() string {
if p := os.Getenv(envProfileKey); p != "" {
return p
}
return defaultProfile
}
// GetTenancyOCID fetches the tenancy OCID (error on failure).
func GetTenancyOCID() (string, error) {
// Use mock function if set (for testing)
if MockGetTenancyOCID != nil {
return MockGetTenancyOCID()
}
// Normal implementation
id, err := LoadOCIConfig().TenancyOCID()
if err != nil {
return "", errors.Wrap(err, "failed to retrieve tenancy OCID from OCI config")
}
return id, nil
}
// LookupTenancyID locates the OCID for a given tenancy name.
// It returns an error if the map cannot be loaded or if the name isn't found.
func LookupTenancyID(tenancyName string) (string, error) {
// Use mock function if set (for testing)
if MockLookupTenancyID != nil {
return MockLookupTenancyID(tenancyName)
}
// Normal implementation
path := tenancyMapPath()
logger.LogWithLevel(logger.Logger, 3, "looking up tenancy in map", "tenancy", tenancyName, "path", path)
tenancies, err := LoadTenancyMap()
if err != nil {
return "", err
}
for _, env := range tenancies {
if env.Tenancy == tenancyName {
logger.LogWithLevel(logger.Logger, 3, "found tenancy", "tenancy", tenancyName, "tenancyID", env.TenancyID)
return env.TenancyID, nil
}
}
lookupErr := fmt.Errorf("tenancy %q not found in %s", tenancyName, path)
logger.Logger.Info("tenancy lookup failed", "error", lookupErr)
return "", errors.Wrap(lookupErr, "tenancy lookup failed - please check that the tenancy name is correct and exists in the mapping file")
}
// LoadTenancyMap loads the tenancy mapping from disk at tenancyMapPath.
// It logs debug information and returns a slice of OciTenancyEnvironment.
func LoadTenancyMap() ([]OCITenancyEnvironment, error) {
path := tenancyMapPath()
logger.LogWithLevel(logger.Logger, 3, "loading tenancy map", "path", path)
if err := ensureFile(path); err != nil {
logger.Logger.Info("tenancy mapping file not found", "error", err)
return nil, errors.Wrapf(err, "tenancy mapping file not found (%s) - this is normal if you're not using tenancy name lookup. To set up the mapping file, create a YAML file at %s or set the %s environment variable to point to your mapping file. The file should contain entries mapping tenancy names to OCIDs. Example:\n- environment: prod\n tenancy: mytenancy\n tenancy_id: ocid1.tenancy.oc1..aaaaaaaabcdefghijklmnopqrstuvwxyz\n realm: oc1\n compartments: mycompartment\n regions: us-ashburn-1", path, DefaultTenancyMapPath, EnvTenancyMapPath)
}
data, err := os.ReadFile(path)
if err != nil {
logger.Logger.Error(err, "failed to read tenancy mapping file", "path", path)
return nil, errors.Wrapf(err, "failed to read tenancy mapping file (%s)", path)
}
var tenancies []OCITenancyEnvironment
if err := yaml.Unmarshal(data, &tenancies); err != nil {
logger.Logger.Error(err, "failed to parse tenancy mapping file", "path", path)
return nil, errors.Wrapf(err, "failed to parse tenancy mapping file (%s) - please check that the file is valid YAML", path)
}
logger.LogWithLevel(logger.Logger, 3, "loaded tenancy mapping entries", "count", len(tenancies))
return tenancies, nil
}
// ensureFile verifies the given path exists and is not a directory.
func ensureFile(path string) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.IsDir() {
return fmt.Errorf("path %s is a directory, expected a file", path)
}
return nil
}
// getUserHomeDir returns the path to the current user's home directory or an error if unable to determine it.
func getUserHomeDir() (string, error) {
dir, err := os.UserHomeDir()
if err != nil {
logger.Logger.Error(err, "failed to get user home directory")
return "", fmt.Errorf("getting user home directory: %w", err)
}
return dir, nil
}
// tenancyMapPath returns either the overridden path or the default.
func tenancyMapPath() string {
if p := os.Getenv(EnvTenancyMapPath); p != "" {
logger.LogWithLevel(logger.Logger, 3, "using tenancy map from env", "path", p)
return p
}
return DefaultTenancyMapPath
}
package logger
import (
"context"
"fmt"
"io"
"log/slog"
"runtime"
"slices"
"sync"
"time"
)
const (
Reset = "\033[0m"
White = "\033[37m"
WhiteDim = "\033[37;2m"
Green = "\033[32m"
GreenDimUnderlined = "\033[32;2;4m"
Magenta = "\033[35m"
BrightRed = "\033[91m"
BrightYellow = "\033[93m"
Cyan = "\033[36m"
CyanDim = "\033[36;2m"
// this mirrors the limit value from the internal slog package
maxBufferSize = 16384
dateFormat = time.Stamp
)
var bufPool = sync.Pool{
New: func() any {
b := make([]byte, 0, 2048)
return &b
},
}
type Options struct {
AddSource bool
Colored bool
Level slog.Leveler
TimeFormat string
}
// Handler is very similar to slog's commonHandler
type Handler struct {
opts Options
json bool
preformattedAttrs []byte
groupPrefix string
groups []string
unopenedGroups []string
nOpenGroups int
mu *sync.Mutex
w io.Writer
}
func NewHandler(out io.Writer, opts Options) *Handler {
return &Handler{
opts: opts,
preformattedAttrs: make([]byte, 0),
unopenedGroups: make([]string, 0),
nOpenGroups: 0,
mu: &sync.Mutex{},
w: out,
}
}
func (h *Handler) clone() *Handler {
return &Handler{
opts: h.opts,
json: h.json,
preformattedAttrs: slices.Clip(h.preformattedAttrs),
groupPrefix: h.groupPrefix,
groups: slices.Clip(h.groups),
nOpenGroups: h.nOpenGroups,
w: h.w,
mu: h.mu,
}
}
func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
minLevel := slog.LevelInfo
if h.opts.Level != nil {
minLevel = h.opts.Level.Level()
}
return level >= minLevel
}
func (h *Handler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
h2 := h.clone()
h2.unopenedGroups = make([]string, len(h.unopenedGroups)+1)
copy(h2.unopenedGroups, h.unopenedGroups)
h2.unopenedGroups[len(h2.unopenedGroups)-1] = name
return h2
}
func (h *Handler) WithAttrs(as []slog.Attr) slog.Handler {
if len(as) == 0 {
return h
}
h2 := h.clone()
h2.preformattedAttrs = h2.appendUnopenedGroups(h2.preformattedAttrs)
h2.unopenedGroups = nil
for _, a := range as {
h2.preformattedAttrs = h2.appendAttr(h2.preformattedAttrs, a)
}
return h2
}
func (h *Handler) appendUnopenedGroups(buf []byte) []byte {
for _, g := range h.unopenedGroups {
buf = fmt.Appendf(buf, "%s ", g)
}
return buf
}
func (h *Handler) appendAttr(buf []byte, a slog.Attr) []byte {
a.Value = a.Value.Resolve()
if a.Equal(slog.Attr{}) {
return buf
}
switch a.Value.Kind() {
case slog.KindGroup:
attrs := a.Value.Group()
if len(attrs) == 0 {
return buf
}
if a.Key != "" {
for _, ga := range attrs {
buf = h.appendAttr(buf, ga)
}
}
default:
if a.Key == "" || a.Value.String() == "" {
return buf
}
buf = h.appendKeyValuePair(buf, a)
}
return buf
}
func (h *Handler) Handle(ctx context.Context, record slog.Record) error {
bufp := bufPool.Get().(*[]byte)
buf := *bufp
defer func() {
*bufp = buf
free(bufp)
}()
// append time, level, then message.
if h.opts.Colored {
buf = fmt.Appendf(buf, WhiteDim)
buf = slog.Time(slog.TimeKey, record.Time).Value.Time().AppendFormat(buf, fmt.Sprintf("%s%s ", dateFormat, Reset))
var color string
switch record.Level {
case slog.LevelDebug:
color = Magenta
case slog.LevelInfo:
color = Green
case slog.LevelWarn:
color = BrightYellow
case slog.LevelError:
color = BrightRed
default:
color = Magenta
}
buf = fmt.Appendf(buf, "%s%s%s ", color, record.Level.String(), Reset)
buf = fmt.Appendf(buf, "%s%s%s ", Cyan, record.Message, Reset)
} else {
buf = slog.Time(slog.TimeKey, record.Time).Value.Time().AppendFormat(buf, fmt.Sprintf("%s ", dateFormat))
buf = fmt.Appendf(buf, "%s ", record.Level)
buf = fmt.Appendf(buf, "%s ", record.Message)
}
if h.opts.AddSource {
buf = h.appendAttr(buf, slog.Any(slog.SourceKey, source(record)))
}
buf = append(buf, h.preformattedAttrs...)
if record.NumAttrs() > 0 {
buf = h.appendUnopenedGroups(buf)
record.Attrs(func(a slog.Attr) bool {
buf = h.appendAttr(buf, a)
return true
})
}
buf = append(buf, "\n"...)
h.mu.Lock()
defer h.mu.Unlock()
_, err := h.w.Write(buf)
return err
}
func (h *Handler) appendKeyValuePair(buf []byte, a slog.Attr) []byte {
if h.opts.Colored {
if a.Key == "err" {
return fmt.Appendf(buf, "%s%s=%v%s ", BrightRed, a.Key, a.Value.String(), Reset)
}
return fmt.Appendf(buf, "%s%s=%s%s%s%s ", WhiteDim, a.Key, Reset, White, a.Value.String(), Reset)
}
return fmt.Appendf(buf, "%s=%v ", a.Key, a.Value.String())
}
func free(b *[]byte) {
if cap(*b) <= maxBufferSize {
*b = (*b)[:0]
bufPool.Put(b)
}
}
func source(r slog.Record) *slog.Source {
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
return &slog.Source{
Function: f.Function,
File: f.File,
Line: f.Line,
}
}
package logger
import (
"fmt"
"github.com/go-logr/logr"
"k8s.io/klog/v2"
"log/slog"
"os"
ctrl "sigs.k8s.io/controller-runtime"
"strings"
)
var (
// Logger is the package-level logger
// It should be initialized using InitLogger before use
Logger logr.Logger
// LogLevel sets the verbosity level for logging
LogLevel string
// CmdLogger is the logger used by command-line operations
CmdLogger logr.Logger
// ColoredOutput determines whether log output should be colored
ColoredOutput bool
// ColoredOutputMsg provides help text for the color flag
ColoredOutputMsg = "Enable colored log messages."
// GLOBAL_VERBOSITY controls the verbosity level for V(n) calls
// Higher values show more verbose logs
GLOBAL_VERBOSITY int
)
// SetLogger initializes the loggers based on the current LogLevel and ColoredOutput settings
func SetLogger() error {
l, err := getSlogLevel(LogLevel)
if err != nil {
return err
}
// Set the global verbosity level based on the log level
switch strings.ToLower(LogLevel) {
case "debug":
// Turn on all verbose levels 0..10 to ensure all debug logs are shown
GLOBAL_VERBOSITY = 10
default:
GLOBAL_VERBOSITY = 0
}
slogger := slog.New(NewHandler(os.Stderr, Options{Level: l, Colored: ColoredOutput}))
kslogger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: getKlogLevel(l)}))
baseLogger := logr.FromSlogHandler(slogger.Handler())
klogger := logr.FromSlogHandler(kslogger.Handler())
klog.SetLogger(klogger)
ctrl.SetLogger(baseLogger)
CmdLogger = baseLogger
return nil
}
// InitLogger initializes the package-level logger
// If no logger is provided, it creates a default one
func InitLogger(logger logr.Logger) {
Logger = logger
if Logger.GetSink() == nil {
// If no logger is provided, or it has a nil sink, create a default one
slogger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
Logger = logr.FromSlogHandler(slogger.Handler())
}
}
// LogWithLevel logs a message at the specified verbosity level.
// If the verbosity level is less than or equal to GLOBAL_VERBOSITY,
// it logs the message using the logger's V(level).Info() method.
// Otherwise, it does nothing.
func LogWithLevel(logger logr.Logger, level int, msg string, keysAndValues ...interface{}) {
if level <= GLOBAL_VERBOSITY {
logger.V(level).Info(msg, keysAndValues...)
}
}
// getSlogLevel converts a string log level to a slog.Level
func getSlogLevel(s string) (slog.Level, error) {
switch strings.ToLower(s) {
case "debug":
// Set to a lower level than LevelDebug to ensure all debug logs are shown
return slog.LevelDebug - 10, nil
case "info":
return slog.LevelInfo, nil
case "warn":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelDebug, fmt.Errorf("%s is not a valid log level", s)
}
}
// For end users, klog messages are mostly useless. I set it to the error level unless debug logging is enabled.
func getKlogLevel(l slog.Level) slog.Level {
if l < slog.LevelInfo {
return l
}
return slog.LevelError
}
package logger
import (
"github.com/go-logr/logr"
"io"
"log/slog"
)
// NewTestLogger creates a logger suitable for testing that doesn't produce output.
// It uses a discard handler to ensure no logs are written to stdout/stderr.
func NewTestLogger() logr.Logger {
// Create a slog.Logger with a handler that discards all output
handler := slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})
slogger := slog.New(handler)
// Convert the slog.Logger to a logr.Logger
return logr.FromSlogHandler(slogger.Handler())
}
package oci
import (
"crypto/rsa"
"fmt"
"github.com/oracle/oci-go-sdk/v65/common"
)
// MockConfigurationProvider is a mock implementation of common.ConfigurationProvider
// for testing purposes. It returns predefined values for all methods.
type MockConfigurationProvider struct {
tenancyID string
userID string
keyFingerprint string
region string
passphrase string
}
// NewMockConfigurationProvider creates a new MockConfigurationProvider with default test values
func NewMockConfigurationProvider() common.ConfigurationProvider {
return &MockConfigurationProvider{
tenancyID: "ocid1.tenancy.oc1..mock-tenancy-id",
userID: "ocid1.user.oc1..mock-user-id",
keyFingerprint: "mock-key-fingerprint",
region: "us-ashburn-1",
passphrase: "",
}
}
// TenancyOCID returns the mock tenancy OCID
func (p *MockConfigurationProvider) TenancyOCID() (string, error) {
return p.tenancyID, nil
}
// UserOCID returns the mock user OCID
func (p *MockConfigurationProvider) UserOCID() (string, error) {
return p.userID, nil
}
// KeyFingerprint returns the mock key fingerprint
func (p *MockConfigurationProvider) KeyFingerprint() (string, error) {
return p.keyFingerprint, nil
}
// Region returns the mock region
func (p *MockConfigurationProvider) Region() (string, error) {
return p.region, nil
}
// KeyID returns a formatted key ID using the mock values
func (p *MockConfigurationProvider) KeyID() (string, error) {
tenancy, err := p.TenancyOCID()
if err != nil {
return "", err
}
user, err := p.UserOCID()
if err != nil {
return "", err
}
fingerprint, err := p.KeyFingerprint()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil
}
// PrivateRSAKey returns a mock private key
func (p *MockConfigurationProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) {
// For testing purposes, we don't need a real private key
// This is just a placeholder that returns nil
return nil, nil
}
// Passphrase returns the mock passphrase
func (p *MockConfigurationProvider) Passphrase() (string, error) {
return p.passphrase, nil
}
// AuthType returns the auth type and configurations
func (p *MockConfigurationProvider) AuthType() (common.AuthConfig, error) {
return common.AuthConfig{
AuthType: common.UserPrincipal,
IsFromConfigFile: false,
}, nil
}
package oci
import (
"fmt"
"github.com/oracle/oci-go-sdk/v65/identity"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/containerengine"
"github.com/oracle/oci-go-sdk/v65/core"
"github.com/oracle/oci-go-sdk/v65/database"
)
// NewIdentityClient creates and returns a new instance of IdentityClient using the provided configuration provider.
func NewIdentityClient(provider common.ConfigurationProvider) (identity.IdentityClient, error) {
client, err := identity.NewIdentityClientWithConfigurationProvider(provider)
if err != nil {
return client, fmt.Errorf("creating identity client: %w", err)
}
return client, nil
}
// NewComputeClient creates a new OCI compute client using the provided configuration provider.
func NewComputeClient(provider common.ConfigurationProvider) (core.ComputeClient, error) {
client, err := core.NewComputeClientWithConfigurationProvider(provider)
if err != nil {
return client, fmt.Errorf("creating compute client: %w", err)
}
return client, nil
}
// NewNetworkClient creates a new OCI virtual network client using the provided configuration provider.
func NewNetworkClient(provider common.ConfigurationProvider) (core.VirtualNetworkClient, error) {
client, err := core.NewVirtualNetworkClientWithConfigurationProvider(provider)
if err != nil {
return client, fmt.Errorf("creating virtual network client: %w", err)
}
return client, nil
}
// NewContainerEngineClient creates a new instance of ContainerEngineClient using the provided configuration provider.
func NewContainerEngineClient(provider common.ConfigurationProvider) (containerengine.ContainerEngineClient, error) {
client, err := containerengine.NewContainerEngineClientWithConfigurationProvider(provider)
if err != nil {
return client, fmt.Errorf("creating container engine client: %w", err)
}
return client, nil
}
// NewDatabaseClient creates and returns a new DatabaseClient using the provided configuration.
func NewDatabaseClient(provider common.ConfigurationProvider) (database.DatabaseClient, error) {
client, err := database.NewDatabaseClientWithConfigurationProvider(provider)
if err != nil {
return client, fmt.Errorf("creating database client: %w", err)
}
return client, nil
}
package printer
import (
"encoding/json"
"fmt"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
"golang.org/x/term"
"io"
"os"
"strings"
"unicode/utf8"
)
// Printer handles formatting and writing output to a designated writer.
type Printer struct {
out io.Writer
}
// New creates a new Printer that writes to the provided io.Writer.
// For console output, use os.Stdout. For testing, use bytes.Buffer.
func New(out io.Writer) *Printer {
return &Printer{out: out}
}
// -----------------------------------------------------------------------------
// Utility helpers
// -----------------------------------------------------------------------------
// getTerminalWidth returns the current terminal width. If the writer is not a
// file descriptor (e.g., in tests) or the call fails, it falls back to 80 cols.
func (p *Printer) getTerminalWidth() int {
if f, ok := p.out.(*os.File); ok {
if w, _, err := term.GetSize(int(f.Fd())); err == nil {
return w
}
}
return 80 // sensible default
}
// truncate shortens a string to max runes, appending an ellipsis when needed.
func truncate(s string, max int) string {
if utf8.RuneCountInString(s) <= max {
return s
}
r := []rune(s)
if max <= 3 {
return string(r[:max])
}
return string(r[:max-3]) + "..."
}
// -----------------------------------------------------------------------------
// JSON output helpers
// -----------------------------------------------------------------------------
// MarshalToJSON marshals data to JSON and writes it to the printer's output.
func (p *Printer) MarshalToJSON(data interface{}) error {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal data to JSON: %w", err)
}
_, err = fmt.Fprintln(p.out, string(jsonData))
return err
}
// -----------------------------------------------------------------------------
// Key/Value table
// -----------------------------------------------------------------------------
// PrintKeyValues renders a table from a map, with ordered keys, a title, and
// colored values.
func (p *Printer) PrintKeyValues(title string, data map[string]string, keys []string) {
termWidth := p.getTerminalWidth()
maxKeyWidth := 20
maxValWidth := termWidth - maxKeyWidth - 10 // Padding/border allowance
if maxValWidth < 20 {
maxValWidth = 20
}
t := table.NewWriter()
t.SetOutputMirror(p.out)
t.SetStyle(table.StyleRounded)
t.Style().Title.Align = text.AlignCenter
t.SetTitle(title)
t.AppendHeader(table.Row{"KEY", "VALUE"})
t.SetColumnConfigs([]table.ColumnConfig{
{
Number: 1,
WidthMax: maxKeyWidth,
Transformer: func(val interface{}) string {
return truncate(fmt.Sprint(val), maxKeyWidth)
},
},
{
Number: 2,
WidthMax: maxValWidth,
Transformer: func(val interface{}) string {
return truncate(fmt.Sprint(val), maxValWidth)
},
},
})
for i, key := range keys {
if value, ok := data[key]; ok {
if i > 0 {
t.AppendSeparator()
}
coloredValue := text.Colors{text.FgYellow}.Sprint(value)
t.AppendRow(table.Row{key, coloredValue})
}
}
t.Render()
}
// -----------------------------------------------------------------------------
// Responsive multi‑column table
// -----------------------------------------------------------------------------
// PrintTable renders a table with the given headers and rows, automatically
// adapting column widths based on the current terminal size.
func (p *Printer) PrintTable(title string, headers []string, rows [][]string) {
termWidth := p.getTerminalWidth()
// Calculate a reasonable max width per column.
// Rough formula: subtract borders/padding (≈3 chars per col), then divide.
pad := (len(headers) + 1) * 3
maxPerCol := (termWidth - pad) / len(headers)
if maxPerCol < 10 {
maxPerCol = 10 // never let columns get absurdly narrow
}
// Set up the table writer
t := table.NewWriter()
t.SetOutputMirror(p.out)
t.SetStyle(table.StyleRounded)
t.Style().Title.Align = text.AlignCenter
t.SetTitle(title)
// Build header row and column configs simultaneously
headerRow := make(table.Row, len(headers))
colConfigs := make([]table.ColumnConfig, len(headers))
for i, h := range headers {
headerRow[i] = text.Colors{text.FgHiYellow}.Sprint(h)
// Capture i and h for the transformer closure
idx := i
colConfigs[i] = table.ColumnConfig{
Number: i + 1, // 1‑based index
WidthMax: maxPerCol,
Transformer: func(val interface{}) string {
return truncate(fmt.Sprint(val), maxPerCol)
},
}
// Special case: align CIDR and IP columns to the center for readability
if h == "CIDR" || strings.Contains(strings.ToLower(h), "ip") {
colConfigs[idx].Align = text.AlignCenter
}
}
t.AppendHeader(headerRow)
t.SetColumnConfigs(colConfigs)
// Add rows
for _, row := range rows {
tblRow := make(table.Row, len(row))
for i, cell := range row {
tblRow[i] = cell
}
t.AppendRow(tblRow)
}
t.Render()
}
package image
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
// FindImages retrieves image matching the provided search pattern and outputs them in either table or JSON format.
// Parameters:
// - appCtx: The application context containing configuration, clients, and logging.
// - searchPattern: The string used to search for matching image.
// - useJSON: A boolean indicating whether to output the result in JSON format.
// Returns an error if the operation fails at any stage (service creation, image retrieval, or output).
func FindImages(appCtx *app.ApplicationContext, searchPattern string, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "FindImage", "json", useJSON)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating compute service: %w", err)
}
ctx := context.Background()
matchedImages, err := service.Find(ctx, searchPattern)
if err != nil {
return fmt.Errorf("finding image: %w", err)
}
err = PrintImagesInfo(matchedImages, appCtx, nil, useJSON)
if err != nil {
return fmt.Errorf("printing image table: %w", err)
}
return nil
}
package image
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// ListImages lists image from the compute service with a provided limit, page, and JSON output option.
// It uses the application context for configuration and logging.
// Returns an error if the operation fails.
func ListImages(appCtx *app.ApplicationContext, limit int, page int, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "ListImages", "limit", limit, "page", page, "json", useJSON)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating compute service: %w", err)
}
ctx := context.Background()
images, totalCount, nextPageToken, err := service.List(ctx, limit, page)
if err != nil {
return fmt.Errorf("listing images: %w", err)
}
// Display image information with pagination details
err = PrintImagesInfo(images, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON)
if err != nil {
return fmt.Errorf("printing image table: %w", err)
}
return nil
}
package image
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// PrintImagesInfo displays instances in a formatted table or JSON format.
// It now returns an error to allow for proper error handling by the caller.
func PrintImagesInfo(images []Image, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
return util.MarshalDataToJSONResponse[Image](p, images, pagination)
}
if util.ValidateAndReportEmpty(images, pagination, appCtx.Stdout) {
return nil
}
// Print each image as a separate key-value table with a colored title.
for _, image := range images {
// Create image data map
imageData := map[string]string{
"Name": image.Name,
"ID": image.ID,
"Created": image.CreatedAt.String(),
"ImageOSVersion": image.ImageOSVersion,
"OperatingSystem": image.OperatingSystem,
"LunchMode": image.LunchMode,
}
// Define ordered keys
orderedKeys := []string{
"Name", "ID", "Created", "ImageName", "ImageOSVersion", "OperatingSystem", "LunchMode",
}
// Create the colored title using components from the app context.
title := util.FormatColoredTitle(appCtx, image.Name)
// Call the printer method to render the key-value table for this instance.
p.PrintKeyValues(title, imageData, orderedKeys)
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
package image
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/oci"
"github.com/cnopslabs/ocloud/internal/services/util"
"github.com/oracle/oci-go-sdk/v65/core"
"strings"
)
// NewService initializes a new Service instance with the provided application context.
// Returns a Service pointer and an error if initialization fails.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
cfg := appCtx.Provider
cc, err := oci.NewComputeClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create compute client: %w", err)
}
return &Service{
compute: cc,
logger: appCtx.Logger,
compartmentID: appCtx.CompartmentID,
}, nil
}
// List retrieves a paginated list of image with given limit and page number parameters.
// It returns the slice of image, total count, next page token, and an error if encountered.
func (s *Service) List(ctx context.Context, limit, pageNum int) ([]Image, int, string, error) {
logger.LogWithLevel(s.logger, 3, "List() called with pagination parameters",
"limit", limit,
"pageNum", pageNum)
var images []Image
var nextPageToken string
var totalCount int
//Create a request with a limit parameter to fetch only the required page
request := core.ListImagesRequest{
CompartmentId: &s.compartmentID,
}
// Add limit parameters if specified
if limit > 0 {
request.Limit = &limit
logger.LogWithLevel(s.logger, 3, "Setting limit parameter", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 3, "Calculating page token for page", "pageNum", pageNum)
// We need to fetch page tokens until we reach the desired page
page := ""
currentPage := 1
for currentPage < pageNum {
// Fetch just the page token, not actual data
// Use the same limit to ensure consistent pagination
tokenRequest := core.ListImagesRequest{
CompartmentId: &s.compartmentID,
Page: &page,
}
if limit > 0 {
tokenRequest.Limit = &limit
}
resp, err := s.compute.ListImages(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there's no next page, we've reached the end
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []Image{}, 0, "", nil
}
// Move to the next page
page = *resp.OpcNextPage
currentPage++
}
// Set the page token for the actual request
request.Page = &page
logger.LogWithLevel(s.logger, 3, "Using page token for page", "pageNum", pageNum, "token", page)
}
// Fetch image for the requested page
resp, err := s.compute.ListImages(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing image: %w", err)
}
// Set the total count to the number of instances returned
// If we have a next page, this is an estimate
totalCount = len(resp.Items)
// If we have a next page, we know there are more instances
if resp.OpcNextPage != nil {
// Estimate total count based on current page and items per rage
totalCount = pageNum*limit + limit
}
// Save the next page token if available
if resp.OpcNextPage != nil {
nextPageToken = *resp.OpcNextPage
logger.LogWithLevel(s.logger, 3, "Next page token", "token", nextPageToken)
}
// Process the image
for _, oc := range resp.Items {
images = append(images, mapToImage(oc))
}
// Calculate if there are more pages after the current page
hasNextPage := pageNum*limit < totalCount
logger.LogWithLevel(s.logger, 2, "Completed instance listing with pagination",
"returnedCount", len(images),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return images, totalCount, nextPageToken, nil
}
// Find performs a fuzzy search for an image using the provided search pattern and context.
// It returns a slice of matching Image objects or an error if the search fails.
func (s *Service) Find(ctx context.Context, searchPattern string) ([]Image, error) {
logger.LogWithLevel(s.logger, 3, "finding image with bleve fuzzy search", "pattern", searchPattern)
var allImages []Image
// 1. Fetch all images
allImages, err := s.fetchAllImages(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch all images: %w", err)
}
// 2. Build index
index, err := util.BuildIndex(allImages, func(img Image) any {
return mapToIndexableImage(img)
})
if err != nil {
return nil, fmt.Errorf("failed to build index: %w", err)
}
// 3. Fuzzy search on multiple fields
fields := []string{"Name", "ImageOSVersion", "OperatingSystem", "LunchMode"}
matchedIdxs, err := util.FuzzySearchIndex(index, strings.ToLower(searchPattern), fields)
if err != nil {
return nil, fmt.Errorf("failed to fuzzy search index: %w", err)
}
// Return matched images
var matchedImages []Image
for _, idx := range matchedIdxs {
if idx >= 0 && idx < len(allImages) {
matchedImages = append(matchedImages, allImages[idx])
}
}
logger.LogWithLevel(s.logger, 2, "found image", "count", len(matchedImages))
return matchedImages, nil
}
// fetchAllImages retrieves all images from the service by paginating through the available pages.
// It returns a slice of Image objects and an error in case of failure.
func (s *Service) fetchAllImages(ctx context.Context) ([]Image, error) {
var allImages []Image
page := ""
for {
resp, err := s.compute.ListImages(ctx, core.ListImagesRequest{
CompartmentId: &s.compartmentID,
Page: &page,
})
if err != nil {
return nil, fmt.Errorf("failed to list image: %w", err)
}
for _, oc := range resp.Items {
allImages = append(allImages, mapToImage(oc))
}
if resp.OpcNextPage == nil {
break
}
page = *resp.OpcNextPage
}
return allImages, nil
}
// mapToImage converts a core.Image object to an Image struct, extracting specific fields for use in the application.
func mapToImage(oc core.Image) Image {
return Image{
ID: *oc.Id,
Name: *oc.DisplayName,
CreatedAt: *oc.TimeCreated,
OperatingSystem: *oc.OperatingSystem,
ImageOSVersion: *oc.OperatingSystemVersion,
LunchMode: string(oc.LaunchMode),
ImageTags: util.ResourceTags{
FreeformTags: oc.FreeformTags,
DefinedTags: oc.DefinedTags,
},
}
}
// mapToIndexableImage converts an Image object into an IndexableImage structure optimized for indexing and searching.
func mapToIndexableImage(img Image) IndexableImage {
flattenedTags, _ := util.FlattenTags(img.ImageTags.FreeformTags, img.ImageTags.DefinedTags)
tagValues, _ := util.ExtractTagValues(img.ImageTags.FreeformTags, img.ImageTags.DefinedTags)
return IndexableImage{
Name: strings.ToLower(img.Name),
ImageOSVersion: strings.ToLower(img.ImageOSVersion),
OperatingSystem: strings.ToLower(img.OperatingSystem),
LunchMode: strings.ToLower(img.LunchMode),
Tags: flattenedTags,
TagValues: tagValues,
}
}
package instance
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
// FindInstances searches for instances in the OCI compartment matching the given name pattern.
// It uses the pre-initialized compute and network clients from the ApplicationContext struct.
// Parameters:
// - appCtx: The application with all clients, logger, and resolved IDs.
// - namePattern: The pattern used to match instance names.
// - showImageDetails: A flag indicating whether to include image details in the output.
// - useJSON: A flag indicating whether to output information in JSON format.
// Returns an error if the operation fails.
func FindInstances(appCtx *app.ApplicationContext, namePattern string, showImageDetails bool, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "FindInstances", "namePattern", namePattern, "showImageDetails", showImageDetails, "json", useJSON)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating compute service: %w", err)
}
ctx := context.Background()
matchedInstances, err := service.Find(ctx, namePattern, showImageDetails)
if err != nil {
return fmt.Errorf("finding instances: %w", err)
}
// TODO:
// Display matched instances
if len(matchedInstances) == 0 {
if useJSON {
// Return an empty JSON array if no instances found
fmt.Fprintln(appCtx.Stdout, `{"instances": [], "pagination": null}`)
} else {
fmt.Fprintf(appCtx.Stdout, "No instances found matching pattern: %s\n", namePattern)
}
return nil
}
// Pass the showImageDetails flag to PrintInstancesInfo
err = PrintInstancesInfo(matchedInstances, appCtx, nil, useJSON, showImageDetails)
if err != nil {
return fmt.Errorf("printing instances table: %w", err)
}
return nil
}
package instance
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// ListInstances lists instances in the configured compartment using the provided application.
// It uses the pre-initialized compute client from the ApplicationContext struct and supports pagination.
func ListInstances(appCtx *app.ApplicationContext, limit int, page int, useJSON bool, showImageDetails bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "ListInstances", "limit", limit, "page", page, "json", useJSON, "showImageDetails", showImageDetails)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating compute service: %w", err)
}
ctx := context.Background()
instances, totalCount, nextPageToken, err := service.List(ctx, limit, page, showImageDetails)
if err != nil {
return fmt.Errorf("listing instances: %w", err)
}
// Display instance information with pagination details
err = PrintInstancesInfo(instances, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON, showImageDetails)
if err != nil {
return fmt.Errorf("printing instances table: %w", err)
}
return nil
}
package instance
import (
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// PrintInstancesInfo displays instances in a formatted table or JSON format.
// It now returns an error to allow for proper error handling by the caller.
func PrintInstancesInfo(instances []Instance, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool, showImageDetails bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
return util.MarshalDataToJSONResponse[Instance](p, instances, pagination)
}
// Handle the case where no instances are found.
if util.ValidateAndReportEmpty(instances, pagination, appCtx.Stdout) {
return nil
}
// Print each instance as a separate key-value table with a colored title.
for _, instance := range instances {
// Create instance data map
instanceData := map[string]string{
"ID": instance.ID,
"Shape": instance.Shape,
"vCPUs": fmt.Sprintf("%d", instance.Resources.VCPUs),
"Created": instance.CreatedAt.String(),
"Subnet ID": instance.SubnetID,
"Name": instance.Name,
"Private IP": instance.IP,
"Memory": fmt.Sprintf("%d GB", int(instance.Resources.MemoryGB)),
"State": string(instance.State),
}
// Define ordered keys
orderedKeys := []string{
"ID", "Name", "Shape", "vCPUs", "Memory",
"Created", "Subnet ID", "Private IP", "State",
"Boot Volume ID", "Boot Volume State",
}
// Add image details if available
if showImageDetails && instance.ImageID != "" {
// Add image ID
instanceData["Image ID"] = instance.ImageID
// Add an operating system if available
if instance.ImageOS != "" {
instanceData["Operating System"] = instance.ImageOS
}
if instance.ImageName != "" {
instanceData["Image Name"] = instance.ImageName
}
//Add AD
if instance.Placement.AvailabilityDomain != "" {
instanceData["AD"] = instance.Placement.AvailabilityDomain
}
// AD FD
if instance.Placement.FaultDomain != "" {
instanceData["FD"] = instance.Placement.FaultDomain
}
if instance.Placement.Region != "" {
instanceData["Region"] = instance.Placement.Region
}
// Add subnet details
if instance.SubnetName != "" {
instanceData["Subnet Name"] = instance.SubnetName
}
if instance.VcnID != "" {
instanceData["VCN ID"] = instance.VcnID
}
if instance.VcnName != "" {
instanceData["VCN Name"] = instance.VcnName
}
// Add hostname
if instance.Hostname != "" {
instanceData["Hostname"] = instance.Hostname
}
// Add private DNS enabled flag
instanceData["Private DNS Enabled"] = fmt.Sprintf("%t", instance.PrivateDNSEnabled)
// Add route table details
if instance.RouteTableID != "" {
instanceData["Route Table ID"] = instance.RouteTableID
}
if instance.RouteTableName != "" {
instanceData["Route Table Name"] = instance.RouteTableName
}
// Add image details to ordered keys
imageKeys := []string{
"Image ID",
"Image Name",
"Operating System",
"AD",
"FD",
"Region",
"Subnet Name",
"VCN ID",
"VCN Name",
"Hostname",
"Private DNS Enabled",
"Route Table ID",
"Route Table Name",
}
// Insert image keys after the "State" key
newOrderedKeys := make([]string, 0, len(orderedKeys)+len(imageKeys))
for _, key := range orderedKeys {
newOrderedKeys = append(newOrderedKeys, key)
if key == "State" {
newOrderedKeys = append(newOrderedKeys, imageKeys...)
}
}
orderedKeys = newOrderedKeys
}
title := util.FormatColoredTitle(appCtx, instance.Name)
// Call the printer method to render the key-value table for this instance.
p.PrintKeyValues(title, instanceData, orderedKeys)
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
package instance
import (
"context"
"fmt"
"github.com/blevesearch/bleve/v2"
"github.com/cnopslabs/ocloud/internal/services/util"
"strconv"
"strings"
"sync"
"time"
"github.com/oracle/oci-go-sdk/v65/core"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/oci"
)
// NewService creates a new Service instance with OCI compute and network clients using the provided ApplicationContext.
// Returns a Service pointer and an error if the initialization fails.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
cfg := appCtx.Provider
cc, err := oci.NewComputeClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create compute client: %w", err)
}
nc, err := oci.NewNetworkClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create network client: %w", err)
}
return &Service{
compute: cc,
network: nc,
logger: appCtx.Logger,
compartmentID: appCtx.CompartmentID,
enableConcurrency: appCtx.EnableConcurrency,
subnetCache: make(map[string]*core.Subnet),
vcnCache: make(map[string]*core.Vcn),
routeTableCache: make(map[string]*core.RouteTable),
pageTokenCache: make(map[string]map[int]string),
}, nil
}
// List retrieves a paginated list of running VM instances within a specified compartment.
// It supports pagination through the use of a limit and page number.
// If showImageDetails is true, it also enriches instances with image details.
// Returns instances, total count, next page token, and an error, if any.
func (s *Service) List(ctx context.Context, limit int, pageNum int, showImageDetails bool) ([]Instance, int, string, error) {
// Log input parameters at debug level
logger.LogWithLevel(s.logger, 3, "List() called with pagination parameters",
"limit", limit,
"pageNum", pageNum)
// Initialize variables
var instances []Instance
instanceMap := make(map[string]*Instance)
var nextPageToken string
var totalCount int
// Create a request with a limit parameter to fetch only the required page
request := core.ListInstancesRequest{
CompartmentId: &s.compartmentID,
LifecycleState: core.InstanceLifecycleStateRunning,
}
// Add limit parameter if specified
if limit > 0 {
request.Limit = &limit
logger.LogWithLevel(s.logger, 3, "Setting limit parameter", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 3, "Calculating page token for page", "pageNum", pageNum)
// Check if we have a cache for this compartment
if _, ok := s.pageTokenCache[s.compartmentID]; !ok {
s.pageTokenCache[s.compartmentID] = make(map[int]string)
}
// Check if we have the page token in the cache
if token, ok := s.pageTokenCache[s.compartmentID][pageNum]; ok {
logger.LogWithLevel(s.logger, 3, "Using cached page token", "pageNum", pageNum, "token", token)
request.Page = &token
} else {
// We need to fetch page tokens until we reach the desired page
page := ""
currentPage := 1
// Check if we have any cached page tokens that can help us get closer to the desired page
var startPage int
var startToken string
for p := pageNum - 1; p >= 1; p-- {
if token, ok := s.pageTokenCache[s.compartmentID][p]; ok {
startPage = p
startToken = token
logger.LogWithLevel(s.logger, 3, "Found cached token for earlier page", "startPage", startPage, "token", startToken)
break
}
}
// If we found a cached token, start from there
if startToken != "" {
page = startToken
currentPage = startPage + 1
}
for currentPage <= pageNum {
// Fetch just the page token, not the actual data
// Use the same limit to ensure consistent pagination
tokenRequest := core.ListInstancesRequest{
CompartmentId: &s.compartmentID,
LifecycleState: core.InstanceLifecycleStateRunning,
Page: &page,
}
if limit > 0 {
tokenRequest.Limit = &limit
}
resp, err := s.compute.ListInstances(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there's no next page, we've reached the end
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []Instance{}, 0, "", nil
}
// Move to the next page
page = *resp.OpcNextPage
// Cache the token for this page
s.pageTokenCache[s.compartmentID][currentPage] = page
logger.LogWithLevel(s.logger, 3, "Cached page token", "page", currentPage, "token", page)
currentPage++
}
// Set the page token for the actual request
// We use the token for the page before the one we want
if token, ok := s.pageTokenCache[s.compartmentID][pageNum-1]; ok {
request.Page = &token
logger.LogWithLevel(s.logger, 3, "Using calculated page token", "pageNum", pageNum, "token", token)
}
}
}
// Fetch the instances for the requested page
resp, err := s.compute.ListInstances(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing instances: %w", err)
}
// Set the total count to the number of instances returned
// If we have a next page, this is an estimate
totalCount = len(resp.Items)
// If we have a next page, we know there are more instances
if resp.OpcNextPage != nil {
// If we have a next page token, we know there are more instances
// We need to estimate the total count more accurately
// Since we don't know the exact total count, we'll set it to a value
// that indicates there are more pages (at least one more page worth of instances)
totalCount = pageNum*limit + limit
}
// Save the next page token if available
if resp.OpcNextPage != nil {
nextPageToken = *resp.OpcNextPage
logger.LogWithLevel(s.logger, 3, "Next page token", "token", nextPageToken)
}
// Process the instances
for _, oc := range resp.Items {
inst := mapToInstance(oc)
instances = append(instances, inst)
// Create a copy of the instance and store a pointer to it in the map
// This ensures the pointer remains valid even if the slice is reallocated
instanceCopy := inst
instanceMap[*oc.Id] = &instanceCopy
}
logger.LogWithLevel(s.logger, 3, "Fetched instances for page",
"pageNum", pageNum, "count", len(instances))
// Step 2: Fetch VNIC attachments for the instances in the current page
if len(instanceMap) > 0 {
err := s.enrichInstancesWithVnics(ctx, instanceMap)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error enriching instances with VNICs", "error", err)
// Continue with the instances we have, even if VNIC enrichment failed
}
// Step 3: Fetch image details if requested
if showImageDetails {
err := s.enrichInstancesWithImageDetails(ctx, instanceMap)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error enriching instances with image details", "error", err)
// Continue with the instances we have, even if image details enrichment failed
}
}
}
// Update the instance slice with the enriched data from the instanceMap
// This ensures that the returned instances have the enriched data
for i := range instances {
if enriched, ok := instanceMap[instances[i].ID]; ok {
instances[i] = *enriched
}
}
// Calculate if there are more pages after the current page
// The most direct way to determine if there are more pages is to check if there's a next page token
hasNextPage := resp.OpcNextPage != nil
// Log detailed pagination information at debug level 1 for better visibility
if hasNextPage {
logger.LogWithLevel(s.logger, 1, "Pagination information",
"currentPage", pageNum,
"recordsOnThisPage", len(instances),
"estimatedTotalRecords", totalCount,
"morePages", "true")
}
logger.LogWithLevel(s.logger, 2, "Completed instance listing with pagination",
"returnedCount", len(instances),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return instances, totalCount, nextPageToken, nil
}
// Find searches for cloud instances matching the given pattern within the compartment.
// It attempts an exact name match first, followed by a partial match if necessary.
// Instances are enriched with network data (VNICs) before being returned as a list.
// If showImageDetails is true, it also enriches instances with image details.
// It uses token caching to improve performance for subsequent searches.
func (s *Service) Find(ctx context.Context, searchPattern string, showImageDetails bool) ([]Instance, error) {
// Start overall performance tracking
overallStartTime := time.Now()
logger.LogWithLevel(s.logger, 1, "finding instances", "pattern", searchPattern)
var instanceMap = make(map[string]*Instance)
var allInstances []Instance
// Initialize pagination variables
var page string
currentPage := 1
// Check if we have a cache for this compartment
if _, ok := s.pageTokenCache[s.compartmentID]; !ok {
s.pageTokenCache[s.compartmentID] = make(map[int]string)
logger.LogWithLevel(s.logger, 3, "Created new page token cache for compartment", "compartmentID", s.compartmentID)
} else {
logger.LogWithLevel(s.logger, 3, "Using existing page token cache", "compartmentID", s.compartmentID, "cacheSize", len(s.pageTokenCache[s.compartmentID]))
}
// Find the highest cached page number to optimize fetching
var startPage int
var startToken string
for p := range s.pageTokenCache[s.compartmentID] {
if p > startPage {
startPage = p
startToken = s.pageTokenCache[s.compartmentID][p]
}
}
if startToken != "" {
logger.LogWithLevel(s.logger, 3, "Starting from cached token", "page", startPage, "token", startToken)
page = startToken
currentPage = startPage + 1
}
// Record start time for performance tracking
startTime := time.Now()
fetchCount := 0
// Fetch all Instances with token caching
for {
fetchCount++
logger.LogWithLevel(s.logger, 3, "Fetching instances", "page", currentPage, "token", page, "fetchCount", fetchCount)
resp, err := s.compute.ListInstances(ctx, core.ListInstancesRequest{
CompartmentId: &s.compartmentID,
LifecycleState: core.InstanceLifecycleStateRunning,
Page: &page,
})
if err != nil {
return nil, fmt.Errorf("failed listing instances: %w", err)
}
for _, oc := range resp.Items {
inst := mapToInstance(oc)
allInstances = append(allInstances, inst)
// Add a pointer to the instance to the map for enrichment
instanceCopy := inst
instanceMap[*oc.Id] = &instanceCopy
}
if resp.OpcNextPage == nil {
break
}
// Cache the token for this page
page = *resp.OpcNextPage
s.pageTokenCache[s.compartmentID][currentPage] = page
logger.LogWithLevel(s.logger, 3, "Cached page token", "page", currentPage, "token", page)
currentPage++
}
// Log performance metrics for fetching
fetchDuration := time.Since(startTime)
logger.LogWithLevel(s.logger, 1, "Instance fetching performance",
"totalPages", fetchCount,
"totalInstances", len(allInstances),
"duration", fetchDuration,
"startedFromCachedPage", startPage > 0,
"cachedPagesUsed", startPage)
// Track enrichment performance
enrichStartTime := time.Now()
// Enrich with VNICs using the same approach as List
vnicStartTime := time.Now()
if err := s.enrichInstancesWithVnics(ctx, instanceMap); err != nil {
logger.LogWithLevel(s.logger, 1, "failed to enrich VNICs", "error", err)
}
vnicDuration := time.Since(vnicStartTime)
logger.LogWithLevel(s.logger, 1, "VNIC enrichment performance", "duration", vnicDuration, "instanceCount", len(instanceMap))
// Enrich with image details if requested
if showImageDetails {
imageStartTime := time.Now()
if err := s.enrichInstancesWithImageDetails(ctx, instanceMap); err != nil {
logger.LogWithLevel(s.logger, 1, "failed to enrich image details", "error", err)
}
imageDuration := time.Since(imageStartTime)
logger.LogWithLevel(s.logger, 1, "Image details enrichment performance", "duration", imageDuration, "instanceCount", len(instanceMap))
}
totalEnrichDuration := time.Since(enrichStartTime)
logger.LogWithLevel(s.logger, 1, "Total enrichment performance", "duration", totalEnrichDuration, "instanceCount", len(instanceMap))
// Track search performance
searchStartTime := time.Now()
// Create indexable documents from enriched instances
indexingStartTime := time.Now()
var indexableDocs []IndexableInstance
for _, inst := range instanceMap {
indexableDocs = append(indexableDocs, mapToIndexableInstance(*inst))
}
indexingDuration := time.Since(indexingStartTime)
logger.LogWithLevel(s.logger, 3, "Created indexable documents", "count", len(indexableDocs), "duration", indexingDuration)
// Create an in memory Bleve index
indexCreationStartTime := time.Now()
indexMapping := bleve.NewIndexMapping()
index, err := bleve.NewMemOnly(indexMapping)
if err != nil {
return nil, fmt.Errorf("failed to create Bleve index for instances: %w", err)
}
// Index all documents
for i, doc := range indexableDocs {
err := index.Index(fmt.Sprintf("%d", i), doc)
if err != nil {
return nil, fmt.Errorf("failed to index instances: %w", err)
}
}
indexCreationDuration := time.Since(indexCreationStartTime)
logger.LogWithLevel(s.logger, 3, "Created search index", "duration", indexCreationDuration)
// Prepare a fuzzy query with wildcard
queryStartTime := time.Now()
searchPattern = strings.ToLower(searchPattern)
if !strings.HasPrefix(searchPattern, "*") {
searchPattern = "*" + searchPattern
}
if !strings.HasSuffix(searchPattern, "*") {
searchPattern = searchPattern + "*"
}
// Create a query that searches across all relevant fields
// The _all field is a special field that searches across all indexed fields
// We also explicitly search in Tags and TagValues fields to ensure tag searches work correctly
queryString := fmt.Sprintf("_all:%s OR Tags:%s OR TagValues:%s",
searchPattern, searchPattern, searchPattern)
query := bleve.NewQueryStringQuery(queryString)
searchRequest := bleve.NewSearchRequest(query)
searchRequest.Size = 1000 // Increase from default of 10
queryDuration := time.Since(queryStartTime)
logger.LogWithLevel(s.logger, 3, "Prepared search query", "query", queryString, "duration", queryDuration)
// Perform search
searchExecutionStartTime := time.Now()
result, err := index.Search(searchRequest)
if err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
searchExecutionDuration := time.Since(searchExecutionStartTime)
logger.LogWithLevel(s.logger, 3, "Executed search", "hits", len(result.Hits), "duration", searchExecutionDuration)
// Collect matched results
resultCollectionStartTime := time.Now()
var matched []Instance
for _, hit := range result.Hits {
idx, err := strconv.Atoi(hit.ID)
if err != nil || idx < 0 || idx >= len(indexableDocs) {
continue
}
// Get the instance ID from the indexable document
instanceID := indexableDocs[idx].ID
// Get the enriched instance from the map
if enriched, ok := instanceMap[instanceID]; ok {
matched = append(matched, *enriched)
}
}
resultCollectionDuration := time.Since(resultCollectionStartTime)
logger.LogWithLevel(s.logger, 3, "Collected search results", "matchedCount", len(matched), "duration", resultCollectionDuration)
// Log overall search performance
totalSearchDuration := time.Since(searchStartTime)
logger.LogWithLevel(s.logger, 1, "Search performance",
"totalDuration", totalSearchDuration,
"indexingDuration", indexingDuration,
"indexCreationDuration", indexCreationDuration,
"queryDuration", queryDuration,
"searchExecutionDuration", searchExecutionDuration,
"resultCollectionDuration", resultCollectionDuration,
"matchedCount", len(matched),
"totalInstancesSearched", len(allInstances))
// Log overall performance for the entire Find operation
overallDuration := time.Since(overallStartTime)
logger.LogWithLevel(s.logger, 1, "Overall Find operation performance",
"totalDuration", overallDuration,
"matchedCount", len(matched),
"totalInstancesSearched", len(allInstances),
"startedFromCachedPage", startPage > 0,
"cachedPagesUsed", startPage)
logger.LogWithLevel(s.logger, 2, "found instances", "count", len(matched))
return matched, nil
}
// enrichInstancesWithImageDetails enriches each instance in the provided map with its associated image details.
// This method fetches image details for each instance either concurrently or sequentially based on configuration.
func (s *Service) enrichInstancesWithImageDetails(ctx context.Context, instanceMap map[string]*Instance) error {
if s.enableConcurrency {
logger.LogWithLevel(s.logger, 1, "processing image details in parallel (concurrency enabled)")
var wg sync.WaitGroup
var mu sync.Mutex
for _, inst := range instanceMap {
wg.Add(1)
go func(inst *Instance) {
defer wg.Done()
imageDetails, err := s.fetchImageDetails(ctx, inst.ImageID)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching image details", "imageID", inst.ImageID, "error", err)
return
}
if imageDetails != nil {
mu.Lock()
if imageDetails.DisplayName != nil {
inst.ImageName = *imageDetails.DisplayName
}
if imageDetails.OperatingSystem != nil {
inst.ImageOS = *imageDetails.OperatingSystem
}
// Copy free-form tags
if len(imageDetails.FreeformTags) > 0 {
inst.InstanceTags.FreeformTags = make(map[string]string)
for k, v := range imageDetails.FreeformTags {
inst.InstanceTags.FreeformTags[k] = v
}
logger.LogWithLevel(s.logger, 1, "freeform tags", "tags", inst.InstanceTags.FreeformTags)
}
// Copy defined tags
if len(imageDetails.DefinedTags) > 0 {
inst.InstanceTags.DefinedTags = make(map[string]map[string]interface{})
for namespace, tags := range imageDetails.DefinedTags {
inst.InstanceTags.DefinedTags[namespace] = make(map[string]interface{})
for k, v := range tags {
inst.InstanceTags.DefinedTags[namespace][k] = v
}
}
logger.LogWithLevel(s.logger, 1, "defined tags", "tags", inst.InstanceTags.DefinedTags)
}
mu.Unlock()
}
}(inst)
}
wg.Wait()
} else {
logger.LogWithLevel(s.logger, 1, "processing image details sequentially (concurrency disabled)")
for _, inst := range instanceMap {
imageDetails, err := s.fetchImageDetails(ctx, inst.ImageID)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching image details", "imageID", inst.ImageID, "error", err)
continue
}
if imageDetails != nil {
if imageDetails.DisplayName != nil {
inst.ImageName = *imageDetails.DisplayName
}
if imageDetails.OperatingSystem != nil {
inst.ImageOS = *imageDetails.OperatingSystem
}
// Copy free-form tags
if len(imageDetails.FreeformTags) > 0 {
inst.InstanceTags.FreeformTags = make(map[string]string)
for k, v := range imageDetails.FreeformTags {
inst.InstanceTags.FreeformTags[k] = v
}
logger.LogWithLevel(s.logger, 1, "freeform tags", "tags", inst.InstanceTags.FreeformTags)
}
// Copy defined tags
if len(imageDetails.DefinedTags) > 0 {
inst.InstanceTags.DefinedTags = make(map[string]map[string]interface{})
for namespace, tags := range imageDetails.DefinedTags {
inst.InstanceTags.DefinedTags[namespace] = make(map[string]interface{})
for k, v := range tags {
inst.InstanceTags.DefinedTags[namespace][k] = v
}
}
logger.LogWithLevel(s.logger, 1, "defined tags", "tags", inst.InstanceTags.DefinedTags)
}
}
}
}
return nil
}
// fetchImageDetails fetches the details of an image from OCI
func (s *Service) fetchImageDetails(ctx context.Context, imageID string) (*core.Image, error) {
if imageID == "" {
return nil, nil
}
// Create a request to get the image details
request := core.GetImageRequest{
ImageId: &imageID,
}
// Call the OCI API to get the image details
response, err := s.compute.GetImage(ctx, request)
if err != nil {
return nil, fmt.Errorf("getting image details: %w", err)
}
return &response.Image, nil
}
// enrichInstancesWithVnics enriches each instance in the provided map with its associated VNIC information.
// This method uses a batch approach to fetch VNIC attachments for all instances at once, reducing API calls.
func (s *Service) enrichInstancesWithVnics(ctx context.Context, instanceMap map[string]*Instance) error {
// Extract instance IDs
var instanceIDs []string
for id := range instanceMap {
instanceIDs = append(instanceIDs, id)
}
// Batch fetches VNIC attachments for all instances
logger.LogWithLevel(s.logger, 1, "batch fetching VNIC attachments", "instanceCount", len(instanceIDs))
attachmentsMap, err := s.batchFetchVnicAttachments(ctx, instanceIDs)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error batch fetching VNIC attachments", "error", err)
// Continue with individual fetching as fallback
}
if s.enableConcurrency {
logger.LogWithLevel(s.logger, 1, "processing VNICs in parallel (concurrency enabled)")
var wg sync.WaitGroup
var mu sync.Mutex
for _, inst := range instanceMap {
wg.Add(1)
go func(inst *Instance) {
defer wg.Done()
// Try to get VNIC attachments from the batch result first
var vnic *core.Vnic
if attachments, ok := attachmentsMap[inst.ID]; ok && len(attachments) > 0 {
// Find primary VNIC from attachments
for _, attach := range attachments {
primaryVnic, err := s.getPrimaryVnic(ctx, attach)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error getting primary VNIC from batch", "instanceID", inst.ID, "error", err)
continue
}
if primaryVnic != nil {
vnic = primaryVnic
break
}
}
}
// Fallback to individual fetch if not found in a batch
if vnic == nil {
var err error
vnic, err = s.fetchPrimaryVnicForInstance(ctx, inst.ID)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching primary VNIC", "instanceID", inst.ID, "error", err)
return
}
}
if vnic != nil {
mu.Lock()
// Basic VNIC information
inst.IP = *vnic.PrivateIp
inst.SubnetID = *vnic.SubnetId
// Set hostname if available
if vnic.HostnameLabel != nil {
inst.Hostname = *vnic.HostnameLabel
}
// Fetch subnet details
subnetDetails, err := s.fetchSubnetDetails(ctx, *vnic.SubnetId)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching subnet details", "subnetID", *vnic.SubnetId, "error", err)
} else if subnetDetails != nil {
// Set subnet name
if subnetDetails.DisplayName != nil {
inst.SubnetName = *subnetDetails.DisplayName
}
// Set VCN ID
if subnetDetails.VcnId != nil {
inst.VcnID = *subnetDetails.VcnId
// Fetch VCN details
vcnDetails, err := s.fetchVcnDetails(ctx, *subnetDetails.VcnId)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching VCN details", "vcnID", *subnetDetails.VcnId, "error", err)
} else if vcnDetails != nil && vcnDetails.DisplayName != nil {
inst.VcnName = *vcnDetails.DisplayName
}
}
// Set private DNS enabled flag
if subnetDetails.DnsLabel != nil && *subnetDetails.DnsLabel != "" {
inst.PrivateDNSEnabled = true
}
// Set a route table ID and name
if subnetDetails.RouteTableId != nil {
inst.RouteTableID = *subnetDetails.RouteTableId
// Fetch route table details
routeTableDetails, err := s.fetchRouteTableDetails(ctx, *subnetDetails.RouteTableId)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching route table details", "routeTableID", *subnetDetails.RouteTableId, "error", err)
} else if routeTableDetails != nil && routeTableDetails.DisplayName != nil {
inst.RouteTableName = *routeTableDetails.DisplayName
}
}
}
mu.Unlock()
}
}(inst)
}
wg.Wait()
} else {
logger.LogWithLevel(s.logger, 1, "processing VNICs sequentially (concurrency disabled)")
for _, inst := range instanceMap {
// Try to get VNIC attachments from the batch result first
var vnic *core.Vnic
if attachments, ok := attachmentsMap[inst.ID]; ok && len(attachments) > 0 {
// Find primary VNIC from attachments
for _, attach := range attachments {
primaryVnic, err := s.getPrimaryVnic(ctx, attach)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error getting primary VNIC from batch", "instanceID", inst.ID, "error", err)
continue
}
if primaryVnic != nil {
vnic = primaryVnic
break
}
}
}
// Fallback to individual fetch if not found in a batch
if vnic == nil {
var err error
vnic, err = s.fetchPrimaryVnicForInstance(ctx, inst.ID)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching primary VNIC", "instanceID", inst.ID, "error", err)
continue
}
}
if vnic != nil {
// Basic VNIC information
inst.IP = *vnic.PrivateIp
inst.SubnetID = *vnic.SubnetId
// Set hostname if available
if vnic.HostnameLabel != nil {
inst.Hostname = *vnic.HostnameLabel
}
// Fetch subnet details
subnetDetails, err := s.fetchSubnetDetails(ctx, *vnic.SubnetId)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching subnet details", "subnetID", *vnic.SubnetId, "error", err)
} else if subnetDetails != nil {
// Set subnet name
if subnetDetails.DisplayName != nil {
inst.SubnetName = *subnetDetails.DisplayName
}
// Set VCN ID
if subnetDetails.VcnId != nil {
inst.VcnID = *subnetDetails.VcnId
// Fetch VCN details
vcnDetails, err := s.fetchVcnDetails(ctx, *subnetDetails.VcnId)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching VCN details", "vcnID", *subnetDetails.VcnId, "error", err)
} else if vcnDetails != nil && vcnDetails.DisplayName != nil {
inst.VcnName = *vcnDetails.DisplayName
}
}
// Set private DNS enabled flag
if subnetDetails.DnsLabel != nil && *subnetDetails.DnsLabel != "" {
inst.PrivateDNSEnabled = true
}
// Set a route table ID and name
if subnetDetails.RouteTableId != nil {
inst.RouteTableID = *subnetDetails.RouteTableId
// Fetch route table details
routeTableDetails, err := s.fetchRouteTableDetails(ctx, *subnetDetails.RouteTableId)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error fetching route table details", "routeTableID", *subnetDetails.RouteTableId, "error", err)
} else if routeTableDetails != nil && routeTableDetails.DisplayName != nil {
inst.RouteTableName = *routeTableDetails.DisplayName
}
}
}
}
}
}
return nil
}
// fetchPrimaryVnicForInstance finds the primary VNIC for a given instance ID.
// It uses a batch approach to fetch VNIC attachments for multiple instances at once.
func (s *Service) fetchPrimaryVnicForInstance(ctx context.Context, instanceID string) (*core.Vnic, error) {
// Fetch all VNIC attachments for the instance
attachments, err := s.compute.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{
CompartmentId: &s.compartmentID,
InstanceId: &instanceID,
})
if err != nil {
logger.LogWithLevel(s.logger, 1, "error listing VNIC attachments", "instanceID", instanceID, "error", err)
return nil, nil
}
// Find the primary VNIC
for _, attach := range attachments.Items {
vnic, err := s.getPrimaryVnic(ctx, attach)
if err != nil {
logger.LogWithLevel(s.logger, 1, "error getting primary VNIC", "instanceID", instanceID, "error", err)
continue
}
if vnic != nil {
return vnic, nil
}
}
logger.LogWithLevel(s.logger, 1, "no primary VNIC found for instance", "instanceID", instanceID)
return nil, nil
}
// batchFetchVnicAttachments fetches VNIC attachments for multiple instances in a single API call.
// It returns a map of instance ID to VNIC attachments.
func (s *Service) batchFetchVnicAttachments(ctx context.Context, instanceIDs []string) (map[string][]core.VnicAttachment, error) {
result := make(map[string][]core.VnicAttachment)
// Fetch all VNIC attachments for the compartment
var page string
for {
resp, err := s.compute.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{
CompartmentId: &s.compartmentID,
Page: &page,
})
if err != nil {
return nil, fmt.Errorf("listing VNIC attachments: %w", err)
}
// Filter attachments by instance ID
for _, attach := range resp.Items {
if attach.InstanceId == nil {
continue
}
// Check if this attachment belongs to one of our instances
for _, id := range instanceIDs {
if *attach.InstanceId == id {
result[id] = append(result[id], attach)
break
}
}
}
// Check if there are more pages
if resp.OpcNextPage == nil {
break
}
page = *resp.OpcNextPage
}
return result, nil
}
// getPrimaryVnic retrieves the primary VNIC associated with the provided VnicAttachment.
// It returns the VNIC if it is marked as primary, or nil if no primary VNIC is found.
// In case of an error during the VNIC retrieval process, it returns nil.
func (s *Service) getPrimaryVnic(ctx context.Context, attach core.VnicAttachment) (*core.Vnic, error) {
if attach.VnicId == nil {
logger.LogWithLevel(s.logger, 2, "VnicAttachment missing VnicId", "attachment", attach)
return nil, nil
}
resp, err := s.network.GetVnic(ctx, core.GetVnicRequest{VnicId: attach.VnicId})
if err != nil {
logger.LogWithLevel(s.logger, 2, "GetVnic error", "error", err, "vnicID", *attach.VnicId)
return nil, nil
}
vnic := resp.Vnic
if vnic.IsPrimary != nil && *vnic.IsPrimary {
return &vnic, nil
}
logger.LogWithLevel(s.logger, 2, "VnicAttachment missing primary Vnic", "attachment", attach)
return nil, nil
}
// fetchSubnetDetails retrieves the subnet details for the given subnet ID.
// It uses a cache to avoid making repeated API calls for the same subnet.
func (s *Service) fetchSubnetDetails(ctx context.Context, subnetID string) (*core.Subnet, error) {
// Check cache first
if subnet, ok := s.subnetCache[subnetID]; ok {
logger.LogWithLevel(s.logger, 3, "subnet cache hit", "subnetID", subnetID)
return subnet, nil
}
// Cache miss, fetch from API
logger.LogWithLevel(s.logger, 3, "subnet cache miss", "subnetID", subnetID)
resp, err := s.network.GetSubnet(ctx, core.GetSubnetRequest{
SubnetId: &subnetID,
})
if err != nil {
return nil, fmt.Errorf("getting subnet details: %w", err)
}
// Store in cache
s.subnetCache[subnetID] = &resp.Subnet
return &resp.Subnet, nil
}
// fetchVcnDetails retrieves the VCN details for the given VCN ID.
// It uses a cache to avoid making repeated API calls for the same VCN.
func (s *Service) fetchVcnDetails(ctx context.Context, vcnID string) (*core.Vcn, error) {
// Check cache first
if vcn, ok := s.vcnCache[vcnID]; ok {
logger.LogWithLevel(s.logger, 3, "VCN cache hit", "vcnID", vcnID)
return vcn, nil
}
// Cache miss, fetch from API
logger.LogWithLevel(s.logger, 3, "VCN cache miss", "vcnID", vcnID)
resp, err := s.network.GetVcn(ctx, core.GetVcnRequest{
VcnId: &vcnID,
})
if err != nil {
return nil, fmt.Errorf("getting VCN details: %w", err)
}
// Store in cache
s.vcnCache[vcnID] = &resp.Vcn
return &resp.Vcn, nil
}
// fetchRouteTableDetails retrieves the route table details for the given route table ID.
// It uses a cache to avoid making repeated API calls for the same route table.
func (s *Service) fetchRouteTableDetails(ctx context.Context, routeTableID string) (*core.RouteTable, error) {
// Check cache first
if routeTable, ok := s.routeTableCache[routeTableID]; ok {
logger.LogWithLevel(s.logger, 3, "route table cache hit", "routeTableID", routeTableID)
return routeTable, nil
}
// Cache miss, fetch from API
logger.LogWithLevel(s.logger, 3, "route table cache miss", "routeTableID", routeTableID)
resp, err := s.network.GetRouteTable(ctx, core.GetRouteTableRequest{
RtId: &routeTableID,
})
if err != nil {
return nil, fmt.Errorf("getting route table details: %w", err)
}
// Store in cache
s.routeTableCache[routeTableID] = &resp.RouteTable
return &resp.RouteTable, nil
}
// mapToInstance maps SDK Instance to local model.
func mapToInstance(oc core.Instance) Instance {
return Instance{
Name: *oc.DisplayName,
ID: *oc.Id,
IP: "", // to be filled later
Placement: Placement{
Region: *oc.Region,
AvailabilityDomain: *oc.AvailabilityDomain,
FaultDomain: *oc.FaultDomain,
},
Resources: Resources{
VCPUs: int(*oc.ShapeConfig.Vcpus),
MemoryGB: *oc.ShapeConfig.MemoryInGBs,
},
Shape: *oc.Shape,
ImageID: *oc.ImageId,
SubnetID: "", // to be filled later
State: oc.LifecycleState,
CreatedAt: *oc.TimeCreated,
InstanceTags: util.ResourceTags{
FreeformTags: oc.FreeformTags,
DefinedTags: oc.DefinedTags,
},
}
}
// ToIndexableInstance converts an Instance into an IndexableInstance with simplified and normalized fields for indexing.
func mapToIndexableInstance(instance Instance) IndexableInstance {
flattenedTags, _ := util.FlattenTags(instance.InstanceTags.FreeformTags, instance.InstanceTags.DefinedTags)
tagValues, _ := util.ExtractTagValues(instance.InstanceTags.FreeformTags, instance.InstanceTags.DefinedTags)
return IndexableInstance{
ID: instance.ID,
Name: strings.ToLower(instance.Name),
ImageName: strings.ToLower(instance.ImageName),
ImageOperatingSystem: strings.ToLower(instance.ImageOS),
Tags: flattenedTags,
TagValues: tagValues,
}
}
package oke
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
func FindClusters(appCtx *app.ApplicationContext, namePattern string, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "Finding OKE clusters", "pattern", namePattern)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating oke cluster service: %w", err)
}
ctx := context.Background()
clusters, err := service.Find(ctx, namePattern)
if err != nil {
return fmt.Errorf("finding oke clusters: %w", err)
}
// Display matched clusters
if len(clusters) == 0 {
if useJSON {
// Return an empty JSON array if no clusters found
fmt.Fprintln(appCtx.Stdout, `{"clusters": []}`)
} else {
fmt.Fprintf(appCtx.Stdout, "No clusters found matching pattern: %s\n", namePattern)
}
return nil
}
err = PrintOKEInfo(clusters, appCtx, nil, useJSON)
if err != nil {
return fmt.Errorf("printing clusters: %w", err)
}
return nil
}
package oke
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
func ListClusters(appCtx *app.ApplicationContext, useJSON bool, limit, page int) error {
logger.LogWithLevel(appCtx.Logger, 1, "Listing OKE clusters", "limit", limit, "page", page)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating oke cluster service: %w", err)
}
ctx := context.Background()
clusters, totalCount, nextPageToken, err := service.List(ctx, limit, page)
if err != nil {
return fmt.Errorf("listing oke clusters: %w", err)
}
err = PrintOKETable(clusters, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON)
if err != nil {
return fmt.Errorf("printing clusters: %w", err)
}
return nil
}
package oke
import (
"fmt"
"sort"
"strings"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// PrintOKETable groups cluster metadata and node‑pool details into **one**
// responsive table per cluster. The first row summarizes the cluster; the
// following rows list each node pool.
func PrintOKETable(clusters []Cluster, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
p := printer.New(appCtx.Stdout)
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
if useJSON {
return util.MarshalDataToJSONResponse[Cluster](p, clusters, pagination)
}
if util.ValidateAndReportEmpty(clusters, pagination, appCtx.Stdout) {
return nil
}
for _, c := range clusters {
// Header defines unified columns for both cluster + node‑pool rows.
headers := []string{
"Name", // cluster or node‑pool name
"Type", // "Cluster" or "NodePool"
"Version", // k8s version
"Shape/Endpoint", // node shape or cluster endpoint
"Count/Created", // node count or created timestamp
"State", // lifecycle state
}
// Build rows — first row is the cluster summary.
rows := [][]string{
{
c.Name,
"Cluster",
c.Version,
c.PrivateEndpoint,
c.CreatedAt,
string(c.State),
},
}
// Append node‑pool rows.
for _, np := range c.NodePools {
rows = append(rows, []string{
np.Name,
"NodePool",
np.Version,
np.NodeShape,
fmt.Sprintf("%d", np.NodeCount),
string(np.State),
})
}
title := util.FormatColoredTitle(appCtx, fmt.Sprintf("Cluster: %s (%d node pools)", c.Name, len(c.NodePools)))
p.PrintTable(title, headers, rows)
fmt.Fprintln(appCtx.Stdout) // spacer between clusters
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
// PrintOKEInfo prints a detailed, troubleshooting‑oriented view of OKE clusters
// and their node pools. Each cluster is rendered as:
// 1. A summary key/value block containing operationally‑relevant metadata.
// 2. A responsive table listing all node pools with the most useful columns
// for SRE triage.
//
// When --JSON is requested, the function defers to util.MarshalDataToJSONResponse.
func PrintOKEInfo(clusters []Cluster, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
p := printer.New(appCtx.Stdout)
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
if useJSON {
return util.MarshalDataToJSONResponse[Cluster](p, clusters, pagination)
}
if util.ValidateAndReportEmpty(clusters, pagination, appCtx.Stdout) {
return nil
}
// Sort clusters by name for deterministic output.
sort.Slice(clusters, func(i, j int) bool {
return strings.ToLower(clusters[i].Name) < strings.ToLower(clusters[j].Name)
})
for _, c := range clusters {
summary := map[string]string{
"ID": c.ID,
"Name": c.Name,
"K8s Version": c.Version,
"Created": c.CreatedAt,
"State": string(c.State),
"Private Endpoint": c.PrivateEndpoint,
"Node Pools": fmt.Sprintf("%d", len(c.NodePools)),
}
order := []string{"ID", "Name", "K8s Version", "Created", "State", "Private Endpoint", "Node Pools"}
title := util.FormatColoredTitle(appCtx, fmt.Sprintf("Cluster: %s", c.Name))
p.PrintKeyValues(title, summary, order)
fmt.Fprintln(appCtx.Stdout) // spacer
//-----------------------------------------------------------------
// Node pool details (table)
//-----------------------------------------------------------------
if len(c.NodePools) > 0 {
headers := []string{"Node Pool", "Version", "Shape", "OCPUs", "Mem(GB)", "Node Cnt", "State"}
rows := make([][]string, len(c.NodePools))
for i, np := range c.NodePools {
rows[i] = []string{
np.Name,
np.Version,
np.NodeShape,
np.Ocpus,
np.MemoryGB,
fmt.Sprintf("%d", np.NodeCount),
string(np.State),
}
}
tableTitle := util.FormatColoredTitle(appCtx, "Node Pools")
p.PrintTable(tableTitle, headers, rows)
fmt.Fprintln(appCtx.Stdout) // spacer between clusters
}
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
package oke
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/services/util"
"strings"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/containerengine"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/oci"
)
// NewService creates a new Service instance with OCI container engine client using the provided ApplicationContext.
// Returns a Service pointer and an error if the initialization fails.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
cfg := appCtx.Provider
cec, err := oci.NewContainerEngineClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create container engine client: %w", err)
}
return &Service{
containerEngineClient: cec,
logger: appCtx.Logger,
compartmentID: appCtx.CompartmentID,
}, nil
}
// List retrieves OKE clusters within the specified compartment with pagination support.
// It enriches each cluster with its associated node pools.
// Returns a slice of Cluster objects, total count, next page token, and an error, if any.
func (s *Service) List(ctx context.Context, limit, pageNum int) ([]Cluster, int, string, error) {
logger.LogWithLevel(s.logger, 3, "Listing clusters with pagination",
"limit", limit,
"pageNum", pageNum)
var clusters []Cluster
var nextPageToken string
var totalCount int
// Create a request
request := containerengine.ListClustersRequest{
CompartmentId: &s.compartmentID,
}
// Add limit parameter if specified
if limit > 0 {
limitInt := limit
request.Limit = &limitInt
logger.LogWithLevel(s.logger, 3, "Setting limit parameter", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 3, "Calculating page token for page", "pageNum", pageNum)
// We need to fetch page tokens until we reach the desired page
var page *string // Initialize as nil
currentPage := 1
for currentPage < pageNum {
// Fetch just the page token, not actual data
// Use the same limit to ensure consistent pagination
tokenRequest := containerengine.ListClustersRequest{
CompartmentId: &s.compartmentID,
}
// Only set the Page field if we have a valid page token
if page != nil {
tokenRequest.Page = page
}
if limit > 0 {
limitInt := limit
tokenRequest.Limit = &limitInt
}
resp, err := s.containerEngineClient.ListClusters(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there's no next page, we've reached the end
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []Cluster{}, 0, "", nil
}
// Move to the next page
page = resp.OpcNextPage
currentPage++
}
// Set the page token for the actual request
request.Page = page
logger.LogWithLevel(s.logger, 3, "Using page token for page", "pageNum", pageNum, "token", page)
}
// Fetch clusters for the requested page
response, err := s.containerEngineClient.ListClusters(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing clusters: %w", err)
}
// Set the total count to the number of clusters returned
// If we have a next page, this is an estimate
totalCount = len(response.Items)
// If we have a next page, we know there are more clusters
if response.OpcNextPage != nil {
// Estimate total count based on current page and items per page
totalCount = pageNum*limit + limit
}
// Save the next page token if available
if response.OpcNextPage != nil {
nextPageToken = *response.OpcNextPage
logger.LogWithLevel(s.logger, 3, "Next page token", "token", nextPageToken)
}
for _, cluster := range response.Items {
// Create a cluster without node pools first
clusterObj := mapToCluster(cluster)
// Get node pools for this cluster
nodePools, err := s.getClusterNodePools(ctx, *cluster.Id)
if err != nil {
return nil, 0, "", fmt.Errorf("listing node pools: %w", err)
}
// Assign node pools to the cluster
clusterObj.NodePools = nodePools
// Add the cluster to the result
clusters = append(clusters, clusterObj)
}
// Calculate if there are more pages after the current page
hasNextPage := pageNum*limit < totalCount
logger.LogWithLevel(s.logger, 2, "Completed cluster listing with pagination",
"returnedCount", len(clusters),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return clusters, totalCount, nextPageToken, nil
}
// fetchAllClusters retrieves all clusters within the specified compartment using pagination.
// It returns a slice of Cluster objects and an error, if any.
func (s *Service) fetchAllClusters(ctx context.Context) ([]Cluster, error) {
logger.LogWithLevel(s.logger, 3, "Fetching all clusters")
var allClusters []Cluster
var page *string // Initialize as nil
for {
// Create a request with pagination
request := containerengine.ListClustersRequest{
CompartmentId: &s.compartmentID,
}
// Only set the Page field if we have a valid page token
if page != nil {
request.Page = page
}
// Fetch clusters for the current page
response, err := s.containerEngineClient.ListClusters(ctx, request)
if err != nil {
return nil, fmt.Errorf("listing clusters: %w", err)
}
// Process clusters from this page
for _, cluster := range response.Items {
// Create a cluster without node pools first
clusterObj := mapToCluster(cluster)
// Get node pools for this cluster
nodePools, err := s.getClusterNodePools(ctx, *cluster.Id)
if err != nil {
return nil, fmt.Errorf("listing node pools: %w", err)
}
// Assign node pools to the cluster
clusterObj.NodePools = nodePools
// Add the cluster to the result
allClusters = append(allClusters, clusterObj)
}
// If there's no next page, we're done
if response.OpcNextPage == nil {
break
}
// Move to the next page
page = response.OpcNextPage
}
logger.LogWithLevel(s.logger, 2, "Fetched all clusters", "count", len(allClusters))
return allClusters, nil
}
// Find searches for OKE clusters matching the given pattern within the compartment.
// It performs a case-insensitive search on cluster names and node pool names.
// If searchPattern is empty, it returns all clusters.
// Returns a slice of matching Cluster objects and an error, if any.
func (s *Service) Find(ctx context.Context, searchPattern string) ([]Cluster, error) {
logger.LogWithLevel(s.logger, 1, "Finding clusters", "pattern", searchPattern)
// First, get all clusters using fetchAllClusters
clusters, err := s.fetchAllClusters(ctx)
if err != nil {
return nil, fmt.Errorf("listing clusters for search: %w", err)
}
// If a search pattern is empty, return all clusters
if searchPattern == "" {
return clusters, nil
}
// Filter clusters by name (case-insensitive)
var matchedClusters []Cluster
searchPattern = strings.ToLower(searchPattern)
for _, cluster := range clusters {
// Check if the cluster name contains the search pattern
if strings.Contains(strings.ToLower(cluster.Name), searchPattern) {
matchedClusters = append(matchedClusters, cluster)
continue
}
// Check if any node pool name contains the search pattern
for _, nodePool := range cluster.NodePools {
if strings.Contains(strings.ToLower(nodePool.Name), searchPattern) {
matchedClusters = append(matchedClusters, cluster)
break
}
}
}
logger.LogWithLevel(s.logger, 2, "Found clusters", "count", len(matchedClusters))
return matchedClusters, nil
}
// getClusterNodePools retrieves all node pools associated with the specified cluster.
// It returns a slice of NodePool objects and an error, if any.
func (s *Service) getClusterNodePools(ctx context.Context, clusterID string) ([]NodePool, error) {
logger.LogWithLevel(s.logger, 3, "Getting node pools for cluster", "clusterID", clusterID)
var clusterNodePools []NodePool
nodePools, err := s.containerEngineClient.ListNodePools(ctx, containerengine.ListNodePoolsRequest{
CompartmentId: common.String(s.compartmentID),
ClusterId: common.String(clusterID),
})
if err != nil {
return nil, fmt.Errorf("listing node pools: %w", err)
}
for _, nodePool := range nodePools.Items {
clusterNodePools = append(clusterNodePools, mapToNodePool(nodePool))
}
logger.LogWithLevel(s.logger, 3, "Found node pools for cluster", "clusterID", clusterID, "count", len(clusterNodePools))
return clusterNodePools, nil
}
// mapToCluster maps an OCI ClusterSummary to our internal Cluster model.
// It initializes the NodePools field as an empty slice, which will be populated later.
func mapToCluster(cluster containerengine.ClusterSummary) Cluster {
return Cluster{
ID: *cluster.Id,
Name: *cluster.Name,
CreatedAt: cluster.Metadata.TimeCreated.String(),
Version: *cluster.KubernetesVersion,
State: cluster.LifecycleState,
PrivateEndpoint: *cluster.Endpoints.PrivateEndpoint,
VcnID: *cluster.VcnId,
NodePools: []NodePool{},
OKETags: util.ResourceTags{
FreeformTags: cluster.FreeformTags,
DefinedTags: cluster.DefinedTags,
},
}
}
// mapToNodePool maps an OCI NodePoolSummary to our internal NodePool model.
func mapToNodePool(nodePool containerengine.NodePoolSummary) NodePool {
var nodeCount int
if nodePool.NodeConfigDetails != nil && nodePool.NodeConfigDetails.Size != nil {
nodeCount = *nodePool.NodeConfigDetails.Size
} else if nodePool.QuantityPerSubnet != nil {
nodeCount = *nodePool.QuantityPerSubnet * len(nodePool.SubnetIds)
}
// Extract image details from NodeSourceDetails
image := ""
if details, ok := nodePool.NodeSourceDetails.(containerengine.NodeSourceViaImageDetails); ok && details.ImageId != nil {
image = *details.ImageId
}
// Optional custom logic for parsing shapeConfig
ocpus := ""
memory := ""
if nodePool.NodeShapeConfig != nil {
if nodePool.NodeShapeConfig.Ocpus != nil {
ocpus = fmt.Sprintf("%.1f", *nodePool.NodeShapeConfig.Ocpus)
}
if nodePool.NodeShapeConfig.MemoryInGBs != nil {
memory = fmt.Sprintf("%.0f", *nodePool.NodeShapeConfig.MemoryInGBs)
}
}
return NodePool{
Name: *nodePool.Name,
ID: *nodePool.Id,
Version: *nodePool.KubernetesVersion,
State: nodePool.LifecycleState,
NodeShape: *nodePool.NodeShape,
NodeCount: nodeCount,
Image: image,
Ocpus: ocpus,
MemoryGB: memory,
NodeTags: util.ResourceTags{
FreeformTags: nodePool.FreeformTags,
DefinedTags: nodePool.DefinedTags,
},
}
}
package autonomousdb
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
// FindAutonomousDatabases searches for Autonomous Databases matching the provided name pattern in the application context.
// Logs database discovery tasks and can format the result based on the useJSON flag.
// Returns an error if the discovery or result formatting fails.
func FindAutonomousDatabases(appCtx *app.ApplicationContext, namePattern string, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "Finding Autonomous Databases", "pattern", namePattern)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating autonomous database service: %w", err)
}
ctx := context.Background()
matchedDatabases, err := service.Find(ctx, namePattern)
if err != nil {
return fmt.Errorf("finding autonomous databases: %w", err)
}
err = PrintAutonomousDbInfo(matchedDatabases, appCtx, nil, useJSON)
if err != nil {
return fmt.Errorf("printing autonomous databases: %w", err)
}
return nil
}
package autonomousdb
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// ListAutonomousDatabase fetches and lists all autonomous databases within a specified application context.
// appCtx represents the application context containing configuration and client details.
// useJSON if true, outputs the list of databases in JSON format; otherwise, uses a plain-text format.
// Returns an error if the operation fails.
func ListAutonomousDatabase(appCtx *app.ApplicationContext, useJSON bool, limit, page int) error {
logger.LogWithLevel(appCtx.Logger, 1, "Listing Autonomous Databases")
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating autonomous database service: %w", err)
}
ctx := context.Background()
allDatabases, totalCount, nextPageToken, err := service.List(ctx, limit, page)
if err != nil {
return fmt.Errorf("listing autonomous databases: %w", err)
}
// Display image information with pagination details
err = PrintAutonomousDbInfo(allDatabases, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON)
if err != nil {
return fmt.Errorf("printing image table: %w", err)
}
return nil
}
package autonomousdb
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// PrintAutonomousDbInfo displays instances in a formatted table or JSON format.
// It now returns an error to allow for proper error handling by the caller.
func PrintAutonomousDbInfo(databases []AutonomousDatabase, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
// Special case for empty databases list - return empty object
if len(databases) == 0 && pagination == nil {
return p.MarshalToJSON(struct{}{})
}
return util.MarshalDataToJSONResponse[AutonomousDatabase](p, databases, pagination)
}
if util.ValidateAndReportEmpty(databases, pagination, appCtx.Stdout) {
return nil
}
// Print each Compartment as a separate key-value table with a colored title.
for _, database := range databases {
databaseData := map[string]string{
"Private IP": database.PrivateEndpointIp,
"ID": database.ID,
"Private Endpoint": database.PrivateEndpoint,
"High": database.ConnectionStrings["HIGH"],
"Medium": database.ConnectionStrings["MEDIUM"],
"Low": database.ConnectionStrings["LOW"],
}
// Define ordered Keys
orderedKeys := []string{
"Private IP", "ID", "Private Endpoint", "High", "Medium", "Low",
}
title := util.FormatColoredTitle(appCtx, database.Name)
// Call the printer method to tender the key-value table for this instance
p.PrintKeyValues(title, databaseData, orderedKeys)
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
package autonomousdb
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/oci"
"github.com/cnopslabs/ocloud/internal/services/util"
"github.com/oracle/oci-go-sdk/v65/database"
"strings"
)
// NewService initializes a new Service instance with the provided application context.
// Returns a Service pointer and an error if initialization fails.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
cfg := appCtx.Provider
dbClient, err := oci.NewDatabaseClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create database client: %w", err)
}
return &Service{
dbClient: dbClient,
logger: appCtx.Logger,
compartmentID: appCtx.CompartmentID,
}, nil
}
// List retrieves a paginated list of databases with given limit and page number parameters.
// It returns the slice of databases, total count, next page token, and an error if encountered.
func (s *Service) List(ctx context.Context, limit, pageNum int) ([]AutonomousDatabase, int, string, error) {
// Log input parameters at debug level
logger.LogWithLevel(s.logger, 3, "List called with pagination parameters",
"limit", limit,
"pageNum", pageNum)
var databases []AutonomousDatabase
var nextPageToken string
var totalCount int
// Create a request with a limit parameter to fetch only the required page
request := database.ListAutonomousDatabasesRequest{
CompartmentId: &s.compartmentID,
}
// Add limit parameters if specified
if limit > 0 {
request.Limit = &limit
logger.LogWithLevel(s.logger, 3, "Setting limit parameter", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 3, "Calculating page token for page", "pageNum", pageNum)
// We need to fetch page tokens until we reach the desired page
page := ""
currentPage := 1
for currentPage < pageNum {
// Fetch just the page token, not actual data
// Use the same limit to ensure consistent pagination
tokenRequest := database.ListAutonomousDatabasesRequest{
CompartmentId: &s.compartmentID,
Page: &page,
}
if limit > 0 {
tokenRequest.Limit = &limit
}
resp, err := s.dbClient.ListAutonomousDatabases(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there's no next page, we've reached the end
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []AutonomousDatabase{}, 0, "", nil
}
// Move to the next page
page = *resp.OpcNextPage
currentPage++
}
// Set the page token for the actual request
request.Page = &page
logger.LogWithLevel(s.logger, 3, "Using page token for page", "pageNum", pageNum, "token", page)
}
// Fetch database for the request
resp, err := s.dbClient.ListAutonomousDatabases(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing database: %w", err)
}
// Set the total count to the number of instances returned
// If we have a next page, this is an estimate
totalCount = len(resp.Items)
//If we have a next page, we know there are more instances
if resp.OpcNextPage != nil {
// Estimate total count based on current page and items per rage
totalCount = pageNum*limit + limit
}
// Save the next page token if available
if resp.OpcNextPage != nil {
nextPageToken = *resp.OpcNextPage
logger.LogWithLevel(s.logger, 3, "Next page token", "token", nextPageToken)
}
// Process the databases
for _, item := range resp.Items {
databases = append(databases, mapToDatabase(item))
}
// Calculate if there are more pages after the current page
hasNextPage := pageNum*limit < totalCount
logger.LogWithLevel(s.logger, 2, "Completed instance listing with pagination",
"returnedCount", len(databases),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return databases, totalCount, nextPageToken, nil
}
// Find performs a fuzzy search to find autonomous databases matching the given search pattern in their Name field.
func (s *Service) Find(ctx context.Context, searchPattern string) ([]AutonomousDatabase, error) {
logger.LogWithLevel(s.logger, 3, "finding database with bleve fuzzy search", "pattern", searchPattern)
// 1: Fetch all databases
allDatabases, err := s.fetchAllAutonomousDatabases(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch all databases: %w", err)
}
// 2: Build index
index, err := util.BuildIndex(allDatabases, func(db AutonomousDatabase) any {
return mapToIndexableDatabase(db)
})
if err != nil {
return nil, fmt.Errorf("failed to build index: %w", err)
}
// Step 3: Fuzzy search on multiple fields
fields := []string{"Name"}
matchedIdxs, err := util.FuzzySearchIndex(index, strings.ToLower(searchPattern), fields)
if err != nil {
return nil, fmt.Errorf("failed to fuzzy search index: %w", err)
}
var results []AutonomousDatabase
for _, idx := range matchedIdxs {
if idx >= 0 && idx < len(allDatabases) {
results = append(results, allDatabases[idx])
}
}
logger.LogWithLevel(s.logger, 2, "Compartment search complete", "matches", len(results))
return results, nil
}
// fetchAllAutonomousDatabases retrieves all autonomous databases in the specified compartment by paginating through results.
// It returns a slice of AutonomousDatabase and an error if the retrieval fails.
func (s *Service) fetchAllAutonomousDatabases(ctx context.Context) ([]AutonomousDatabase, error) {
var allDatabases []AutonomousDatabase
page := ""
for {
resp, err := s.dbClient.ListAutonomousDatabases(ctx, database.ListAutonomousDatabasesRequest{
CompartmentId: &s.compartmentID,
Page: &page,
})
if err != nil {
return nil, fmt.Errorf("failed to list database: %w", err)
}
for _, item := range resp.Items {
allDatabases = append(allDatabases, mapToDatabase(item))
}
if resp.OpcNextPage == nil {
break
}
page = *resp.OpcNextPage
}
return allDatabases, nil
}
// mapToIndexableDatabase converts an AutonomousDatabase object into an IndexableAutonomousDatabase object.
// It maps only relevant fields required for indexing, such as the database's name.
func mapToIndexableDatabase(db AutonomousDatabase) IndexableAutonomousDatabase {
return IndexableAutonomousDatabase{
Name: db.Name,
}
}
// mapToDatabase transforms a database.AutonomousDatabaseSummary instance into an AutonomousDatabase struct.
func mapToDatabase(db database.AutonomousDatabaseSummary) AutonomousDatabase {
return AutonomousDatabase{
Name: *db.DbName,
ID: *db.Id,
PrivateEndpoint: *db.PrivateEndpoint,
PrivateEndpointIp: *db.PrivateEndpointIp,
ConnectionStrings: db.ConnectionStrings.AllConnectionStrings,
Profiles: db.ConnectionStrings.Profiles,
DatabaseTags: util.ResourceTags{
FreeformTags: db.FreeformTags,
DefinedTags: db.DefinedTags,
},
}
}
package compartment
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
// FindCompartments searches and displays compartments matching a given name pattern with optional JSON formatting.
// It initializes necessary services, performs a fuzzy search, and outputs results using the defined application context.
// Returns an error if service initialization, search, or result rendering fails.
func FindCompartments(appCtx *app.ApplicationContext, namePattern string, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "Finding Compartments", "pattern", namePattern)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating compartment service: %w", err)
}
ctx := context.Background()
matchedCompartments, err := service.Find(ctx, namePattern)
if err != nil {
return fmt.Errorf("finding matched compartments: %w", err)
}
err = PrintCompartmentsInfo(matchedCompartments, appCtx, nil, useJSON)
if err != nil {
return fmt.Errorf("printing matched compartments: %w", err)
}
return nil
}
package compartment
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// ListCompartments retrieves and displays a paginated list of compartments for the given application context.
// It uses the provided limit, page number, and output format (JSON or table) to render the results.
// Returns an error if fetching or displaying compartments fails.
func ListCompartments(appCtx *app.ApplicationContext, useJSON bool, limit, page int) error {
logger.LogWithLevel(appCtx.Logger, 1, "Listing Compartments", "limit", limit, "page", page)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating compartment service: %w", err)
}
ctx := context.Background()
compartments, totalCount, nextPageToken, err := service.List(ctx, limit, page)
if err != nil {
return fmt.Errorf("listing compartments: %w", err)
}
// Display compartment information with pagination details
err = PrintCompartmentsTable(compartments, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON)
if err != nil {
return fmt.Errorf("printing compartments: %w", err)
}
return nil
}
package compartment
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// PrintCompartmentsTable displays a table or JSON representation of compartments based on the provided configuration.
// It optionally includes pagination details and writes to the application's standard output or as structured JSON.
func PrintCompartmentsTable(compartments []Compartment, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
// Special case for empty compartments list - return an empty object
if len(compartments) == 0 && pagination == nil {
return p.MarshalToJSON(struct{}{})
}
return util.MarshalDataToJSONResponse[Compartment](p, compartments, pagination)
}
if util.ValidateAndReportEmpty(compartments, pagination, appCtx.Stdout) {
return nil
}
// Define table headers
headers := []string{"Name", "ID"}
// Create rows for the table
rows := make([][]string, len(compartments))
for i, c := range compartments {
// Create a row for this compartment
rows[i] = []string{
c.Name,
c.ID,
}
}
// Print the table
title := util.FormatColoredTitle(appCtx, "Compartments")
p.PrintTable(title, headers, rows)
util.LogPaginationInfo(pagination, appCtx)
return nil
}
// PrintCompartmentsInfo displays information about a list of compartments in either JSON or formatted table output.
// It accepts a slice of Compartment, application context, pagination info, and a boolean to indicate JSON output.
// It adjusts pagination details, validates empty compartments, and logs pagination info post-output.
// Returns an error if JSON marshalling or output rendering fails.
func PrintCompartmentsInfo(compartments []Compartment, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
// Special case for empty compartments list - return an empty object
if len(compartments) == 0 && pagination == nil {
return p.MarshalToJSON(struct{}{})
}
return util.MarshalDataToJSONResponse[Compartment](p, compartments, pagination)
}
if util.ValidateAndReportEmpty(compartments, pagination, appCtx.Stdout) {
return nil
}
// Print each Compartment as a separate key-value table with a colored title.
for _, compartment := range compartments {
compartmentData := map[string]string{
"Name": compartment.Name,
"ID": compartment.ID,
"Description": compartment.Description,
}
// Define ordered keys
orderedKeys := []string{
"Name", "ID", "Description",
}
title := util.FormatColoredTitle(appCtx, compartment.Name)
// Call the printer method to render the key-value table for this instance.
p.PrintKeyValues(title, compartmentData, orderedKeys)
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
package compartment
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/identity"
"strings"
)
// NewService initializes and returns a new Service instance using the provided ApplicationContext.
// It injects required dependencies such as IdentityClient, Logger, TenancyID, and TenancyName into the Service.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
return &Service{
identityClient: appCtx.IdentityClient,
logger: appCtx.Logger,
TenancyID: appCtx.TenancyID,
TenancyName: appCtx.TenancyName,
}, nil
}
// List retrieves a paginated list of compartments based on the provided limit and page number parameters.
func (s *Service) List(ctx context.Context, limit, pageNum int) ([]Compartment, int, string, error) {
var compartments []Compartment
var nextPageToken string
var totalCount int
logger.LogWithLevel(s.logger, 3, "List compartments", "limit", limit, "pageNum", pageNum, "Total Count", totalCount)
// Prepare the base request
// to Create a request with a limit parameter to fetch only the required page
request := identity.ListCompartmentsRequest{
CompartmentId: &s.TenancyID,
AccessLevel: identity.ListCompartmentsAccessLevelAccessible,
LifecycleState: identity.CompartmentLifecycleStateActive,
CompartmentIdInSubtree: common.Bool(true), // ListCompartments on the tenancy (root compartment) default is false
}
// Add limit parameters
if limit > 0 {
request.Limit = &limit
logger.LogWithLevel(s.logger, 3, "Setting limit parameter", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 3, "Calculating page token for page", "pageNum", pageNum)
// paginate through results; stop when OpcNextPage is nil
page := ""
currentPage := 1
for currentPage < pageNum {
// Fetch page token, not actual data
// Use limit to ensure consistent patination
tokenRequest := identity.ListCompartmentsRequest{
CompartmentId: &s.TenancyID,
AccessLevel: identity.ListCompartmentsAccessLevelAccessible,
LifecycleState: identity.CompartmentLifecycleStateActive,
CompartmentIdInSubtree: common.Bool(true), // ListCompartments on the tenancy (root compartment) default is false
Page: &page,
}
if limit > 0 {
tokenRequest.Limit = &limit
}
resp, err := s.identityClient.ListCompartments(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there is no next page, we've reached the end then
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []Compartment{}, 0, "", nil
}
// Move to the next page
page = *resp.OpcNextPage
currentPage++
}
// Set the page token for the actual request
request.Page = &page
logger.LogWithLevel(s.logger, 3, "Using page token for page", "pageNum", pageNum, "token", page)
}
// Fetch compartments for the request
response, err := s.identityClient.ListCompartments(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing compartments: %w", err)
}
// Set the total count to the number of compartments returned
// If we have a next page, this is an estimate
totalCount = len(response.Items)
// if we have a next page, we know there is more
if response.OpcNextPage != nil {
// If we have a next page token, we know there are more compartments
// We need to estimate the total count more accurately
// Since we don't know the exact total count, we'll set it to a value
// that indicates there are more pages (at least one more page worth of compartments)
totalCount = pageNum*limit + limit
}
// Save the next page token if available
if response.OpcNextPage != nil {
nextPageToken = *response.OpcNextPage
logger.LogWithLevel(s.logger, 3, "Next page token", "token", nextPageToken, "Total Count", totalCount)
}
// Process the compartment
for _, comp := range response.Items {
compartments = append(compartments, mapToCompartment(comp))
}
// Calculate if there are more pages after the current page
hasNextPage := pageNum*limit < totalCount
logger.LogWithLevel(s.logger, 2, "Completed instance listing with pagination",
"returnedCount", len(compartments),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return compartments, totalCount, nextPageToken, nil
}
// Find performs a fuzzy search for compartments based on the provided searchPattern and returns matching compartments.
func (s *Service) Find(ctx context.Context, searchPattern string) ([]Compartment, error) {
logger.LogWithLevel(s.logger, 3, "Finding allCompartments using Bleve fuzzy search", "pattern", searchPattern)
// Step 1: Fetch all allCompartments
allCompartments, err := s.fetchAllCompartments(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch all compartments: %w", err)
}
// Step 2: Build index
index, err := util.BuildIndex(allCompartments, func(c Compartment) any {
return mapToIndexableCompartment(c)
})
if err != nil {
return nil, fmt.Errorf("failed to build index: %w", err)
}
// Step 3: Fuzzy search on multiple fields
fields := []string{"Name", "Description"}
matchedIdxs, err := util.FuzzySearchIndex(index, strings.ToLower(searchPattern), fields)
if err != nil {
return nil, fmt.Errorf("failed to fuzzy search index: %w", err)
}
// Step 4: Return matched allCompartments
var results []Compartment
for _, idx := range matchedIdxs {
if idx >= 0 && idx < len(allCompartments) {
results = append(results, allCompartments[idx])
}
}
logger.LogWithLevel(s.logger, 2, "Compartment search complete", "matches", len(results))
return results, nil
}
// fetchAllCompartments retrieves all active compartments within a tenancy, including nested compartments.
func (s *Service) fetchAllCompartments(ctx context.Context) ([]Compartment, error) {
var all []Compartment
page := ""
for {
resp, err := s.identityClient.ListCompartments(ctx, identity.ListCompartmentsRequest{
CompartmentId: &s.TenancyID,
Page: &page,
AccessLevel: identity.ListCompartmentsAccessLevelAccessible,
LifecycleState: identity.CompartmentLifecycleStateActive,
CompartmentIdInSubtree: common.Bool(true),
})
if err != nil {
return nil, fmt.Errorf("listing compartments: %w", err)
}
for _, item := range resp.Items {
all = append(all, mapToCompartment(item))
}
if resp.OpcNextPage == nil {
break
}
page = *resp.OpcNextPage
}
return all, nil
}
// mapToCompartment maps an identity.Compartment to a Compartment struct, transferring selected field values.
func mapToCompartment(compartment identity.Compartment) Compartment {
return Compartment{
Name: *compartment.Name,
ID: *compartment.Id,
Description: *compartment.Description,
CompartmentTags: util.ResourceTags{
FreeformTags: compartment.FreeformTags,
DefinedTags: compartment.DefinedTags,
},
}
}
// mapToIndexableCompartment converts a Compartment instance to an IndexableCompartment with lowercased fields.
func mapToIndexableCompartment(compartment Compartment) IndexableCompartment {
return IndexableCompartment{
Name: strings.ToLower(compartment.Name),
Description: strings.ToLower(compartment.Description),
}
}
package policy
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
// FindPolicies retrieves and processes policies matching the provided name pattern within the application context.
// appCtx represents the application context with the necessary clients and configurations.
// namePattern specifies the pattern to filter policy names.
// useJSON determines whether the output should be formatted as JSON.
// Returns an error if policy retrieval or processing fails.
func FindPolicies(appCtx *app.ApplicationContext, namePattern string, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "Finding Policies", "pattern", namePattern)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating policies service: %w", err)
}
ctx := context.Background()
matchedPolicies, err := service.Find(ctx, namePattern)
if err != nil {
return fmt.Errorf("finding matched policies: %w", err)
}
err = PrintPolicyInfo(matchedPolicies, appCtx, nil, useJSON)
if err != nil {
return fmt.Errorf("printing matched policies: %w", err)
}
return nil
}
package policy
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// ListPolicies retrieves and displays the policies for a given application context, supporting pagination and JSON output format.
func ListPolicies(appCtx *app.ApplicationContext, useJSON bool, limit, page int) error {
logger.LogWithLevel(appCtx.Logger, 1, "Listing Policies", "limit", limit, "page", page)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating policy service: %w", err)
}
ctx := context.Background()
policies, totalCount, nextPageToken, err := service.List(ctx, limit, page)
if err != nil {
return fmt.Errorf("listing policies: %w", err)
}
// Display policies information with pagination details
err = PrintPolicyInfo(policies, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON)
if err != nil {
return fmt.Errorf("printing policies: %w", err)
}
return nil
}
package policy
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// PrintPolicyInfo prints the details of policies to the standard output or in JSON format.
// If pagination info is provided, it adjusts and logs it. Returns an error if JSON marshaling fails.
func PrintPolicyInfo(policies []Policy, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
return util.MarshalDataToJSONResponse[Policy](p, policies, pagination)
}
if util.ValidateAndReportEmpty(policies, pagination, appCtx.Stdout) {
return nil
}
// Print each policy as a separate key-value table with a colored title,
for _, policy := range policies {
policyData := map[string]string{
"Name": policy.Name,
"ID": policy.ID,
"Description": policy.Description,
}
// Define ordered keys
orderedKeys := []string{
"Name", "ID", "Description",
}
// Create the colored title using components from the app context
title := util.FormatColoredTitle(appCtx, policy.Name)
// Call the printer method to render the key-value from the app context.
p.PrintKeyValues(title, policyData, orderedKeys)
}
util.LogPaginationInfo(pagination, appCtx)
return nil
}
package policy
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
"github.com/oracle/oci-go-sdk/v65/identity"
"strings"
)
// NewService initializes a new Service instance with the provided application context.
// Returns a Service pointer and an error if initialization fails.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
return &Service{
identityClient: appCtx.IdentityClient,
logger: appCtx.Logger,
CompartmentID: appCtx.CompartmentID,
}, nil
}
// List retrieves a paginated list of policies based on the provided limit and page number parameters.
func (s *Service) List(ctx context.Context, limit, pageNum int) ([]Policy, int, string, error) {
logger.LogWithLevel(s.logger, 1, "Listing Policies", "limit", limit, "page", pageNum)
var policies []Policy
var nextPageToken string
var totalCount int
// Prepare the base request
// Create a request with a limited parameter to fetch only the required page
request := identity.ListPoliciesRequest{
CompartmentId: &s.CompartmentID,
}
// Add limit parameters if specified
if limit > 0 {
request.Limit = &limit
logger.LogWithLevel(s.logger, 1, "Limiting policies to", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 1, "Calculating page token for page", "pageNum", pageNum)
// We need to fetch page tokens until we reach the desired page
page := ""
currentPage := 1
for currentPage < pageNum {
// Fetch Just the page token, not actual data
// Usu the same limit to ensure consistent pagination
tokenRequest := identity.ListPoliciesRequest{
CompartmentId: &s.CompartmentID,
Page: &page,
}
if limit > 0 {
tokenRequest.Limit = &limit
}
resp, err := s.identityClient.ListPolicies(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there's no next page, we've reached the end
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []Policy{}, 0, "", nil
}
// Move to the next page
page = *resp.OpcNextPage
currentPage++
}
// Set the page token for the actual request
request.Page = &page
logger.LogWithLevel(s.logger, 1, "Using page token for page", "pageNum", pageNum, "token", page)
}
// Fetch Policies for the request
resp, err := s.identityClient.ListPolicies(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing policies: %w", err)
}
// Set the total count to the number of policies returned
// If we have a next page, this is an estimate
totalCount = len(resp.Items)
// If we have a next page, we know there are more instances
if resp.OpcNextPage != nil {
// Estimate total count based on current page and items per rage
totalCount = pageNum*limit + limit
}
// Save the next page token if available
if resp.OpcNextPage != nil {
nextPageToken = *resp.OpcNextPage
logger.LogWithLevel(s.logger, 1, "Next page token", "token", nextPageToken)
}
// Process the policies
for _, p := range resp.Items {
policies = append(policies, mapToPolicies(p))
}
// Calculate if there are more pages after the current page
hasNextPage := pageNum*limit < totalCount
logger.LogWithLevel(s.logger, 2, "Completed instance listing with pagination",
"returnedCount", len(policies),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return policies, totalCount, nextPageToken, nil
}
// Find performs a fuzzy search for policies based on the provided searchPattern and returns matching policy.
func (s *Service) Find(ctx context.Context, searchPattern string) ([]Policy, error) {
logger.LogWithLevel(s.logger, 1, "Finding Policies", "pattern", searchPattern)
var allPolicies []Policy
// 1. Fetch all policies in the compartment
allPolicies, err := s.fetchAllPolicies(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch all policies: %w", err)
}
// 2. Build index
index, err := util.BuildIndex(allPolicies, func(p Policy) any {
return mapToIndexablePolicy(p)
})
if err != nil {
return nil, fmt.Errorf("failed to build index: %w", err)
}
// 3. Fuzzy search on multiple files
fields := []string{"Name", "Description", "Statement", "Tags", "TagValues"}
matchedIdxs, err := util.FuzzySearchIndex(index, strings.ToLower(searchPattern), fields)
if err != nil {
return nil, fmt.Errorf("failed to fuzzy search index: %w", err)
}
// Return matched policies
var matchedPolicies []Policy
for _, idx := range matchedIdxs {
if idx >= 0 && idx < len(allPolicies) {
matchedPolicies = append(matchedPolicies, allPolicies[idx])
}
}
logger.LogWithLevel(s.logger, 2, "Found policies", "count", len(matchedPolicies))
return matchedPolicies, nil
}
// fetchAllPolicies retrieves all policies within a specific compartment using pagination. Returns a slice of Policy or an error.
func (s *Service) fetchAllPolicies(ctx context.Context) ([]Policy, error) {
var allPolicies []Policy
page := ""
for {
resp, err := s.identityClient.ListPolicies(ctx, identity.ListPoliciesRequest{
CompartmentId: &s.CompartmentID,
Page: &page,
})
if err != nil {
return nil, fmt.Errorf("failed to list policies: %w", err)
}
for _, p := range resp.Items {
allPolicies = append(allPolicies, mapToPolicies(p))
}
if resp.OpcNextPage == nil {
break
}
page = *resp.OpcNextPage
}
return allPolicies, nil
}
// mapToPolicies converts an identity.Policy object to an internal Policy representation, mapping all fields correspondingly.
func mapToPolicies(policy identity.Policy) Policy {
return Policy{
Name: *policy.Name,
ID: *policy.Id,
Statement: policy.Statements,
Description: *policy.Description,
PolicyTags: util.ResourceTags{
FreeformTags: policy.FreeformTags,
DefinedTags: policy.DefinedTags,
},
}
}
// mapToIndexablePolicy transforms a Policy object into an IndexablePolicy object with indexed and searchable fields.
func mapToIndexablePolicy(p Policy) IndexablePolicy {
flattenedTags, _ := util.FlattenTags(p.PolicyTags.FreeformTags, p.PolicyTags.DefinedTags)
tagValues, _ := util.ExtractTagValues(p.PolicyTags.FreeformTags, p.PolicyTags.DefinedTags)
joinedStatements := strings.Join(p.Statement, " ") // Concatenate all the statements into one string for fuzzy search/indexing.
return IndexablePolicy{
Name: p.Name,
Description: p.Description,
Statement: joinedStatements,
Tags: flattenedTags,
TagValues: tagValues,
}
}
package subnet
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
)
// FindSubnets finds and displays subnets based on the provided name pattern, optionally in JSON format.
// It uses the application context for accessing configurations, logging, and output streams.
// Returns an error if any operation fails during subnet retrieval or output generation.
func FindSubnets(appCtx *app.ApplicationContext, namePattern string, useJSON bool) error {
logger.LogWithLevel(appCtx.Logger, 1, "Finding Subnets", "pattern", namePattern)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating subnet service: %w", err)
}
ctx := context.Background()
matchedSubnets, err := service.Find(ctx, namePattern)
if err != nil {
return fmt.Errorf("finding matched subnets: %w", err)
}
err = PrintSubnetInfo(matchedSubnets, appCtx, useJSON)
if err != nil {
return fmt.Errorf("printing matched subnets: %w", err)
}
return nil
}
package subnet
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/services/util"
)
// ListSubnets retrieves and displays a list of subnets, utilizing pagination, sorting, and optional JSON output.
// Parameters include an application context, a flag for JSON output, limit, page number, and a sorting key.
// Returns an error if any issue occurs during retrieval or output generation.
func ListSubnets(appCtx *app.ApplicationContext, useJSON bool, limit, page int, sortBy string) error {
logger.LogWithLevel(appCtx.Logger, 1, "Listing Subnets", "limit", limit, "page", page, "sortBy", sortBy)
service, err := NewService(appCtx)
if err != nil {
return fmt.Errorf("creating subnet service: %w", err)
}
ctx := context.Background()
policies, totalCount, nextPageToken, err := service.List(ctx, limit, page)
if err != nil {
return fmt.Errorf("listing subnets: %w", err)
}
// Display policies information with pagination details
err = PrintSubnetTable(policies, appCtx, &util.PaginationInfo{
CurrentPage: page,
TotalCount: totalCount,
Limit: limit,
NextPageToken: nextPageToken,
}, useJSON, sortBy)
if err != nil {
return fmt.Errorf("printing subnets: %w", err)
}
return nil
}
package subnet
import (
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/cnopslabs/ocloud/internal/services/util"
"sort"
"strings"
)
// PrintSubnetTable displays a table of subnets with details such as name, CIDR, and DNS info.
// Supports JSON output, pagination, and sorting by specified fields.
// Returns an error if data marshaling or printing fails.
func PrintSubnetTable(subnets []Subnet, appCtx *app.ApplicationContext, pagination *util.PaginationInfo, useJSON bool, sortBy string) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// Adjust the pagination information if available
if pagination != nil {
util.AdjustPaginationInfo(pagination)
}
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
// Special case for empty compartments list - return an empty object
if len(subnets) == 0 && pagination == nil {
return p.MarshalToJSON(struct{}{})
}
return util.MarshalDataToJSONResponse[Subnet](p, subnets, pagination)
}
if util.ValidateAndReportEmpty(subnets, pagination, appCtx.Stdout) {
return nil
}
// Sort subnets based on sortBy parameter
if sortBy != "" {
sortBy = strings.ToLower(sortBy)
switch sortBy {
case "name":
sort.Slice(subnets, func(i, j int) bool {
return strings.ToLower(subnets[i].Name) < strings.ToLower(subnets[j].Name)
})
case "cidr":
sort.Slice(subnets, func(i, j int) bool {
return subnets[i].CIDR < subnets[j].CIDR
})
}
}
// Define table headers
headers := []string{"Name", "CIDR", "Public IP", "DNS Label", "Subnet Domain"}
// Create rows for the table
rows := make([][]string, len(subnets))
for i, subnet := range subnets {
// Determine if public IP is allowed
publicIPAllowed := "No"
if !subnet.ProhibitPublicIPOnVnic {
publicIPAllowed = "Yes"
}
// Create a row for this subnet
rows[i] = []string{
subnet.Name,
subnet.CIDR,
publicIPAllowed,
subnet.DNSLabel,
subnet.SubnetDomainName,
}
}
// Print the table
title := util.FormatColoredTitle(appCtx, "Subnets")
p.PrintTable(title, headers, rows)
util.LogPaginationInfo(pagination, appCtx)
return nil
}
// PrintSubnetInfo displays information about a list of subnets in either JSON format or a formatted table view.
// Parameters:
// - subnets: A slice of Subnet structs containing data to display.
// - appCtx: A pointer to the application context, used for output and configuration.
// - useJSON: A boolean indicating whether the output should be in JSON format.
// Returns an error if JSON marshaling or other operations fail.
func PrintSubnetInfo(subnets []Subnet, appCtx *app.ApplicationContext, useJSON bool) error {
// Create a new printer that writes to the application's standard output.
p := printer.New(appCtx.Stdout)
// If JSON output is requested, use the printer to marshal the response.
if useJSON {
return util.MarshalDataToJSONResponse[Subnet](p, subnets, nil)
}
if util.ValidateAndReportEmpty(subnets, nil, appCtx.Stdout) {
return nil
}
// Print each policy as a separate key-value table with a colored title,
for _, subnet := range subnets {
publicIPAllowed := "No"
if !subnet.ProhibitPublicIPOnVnic {
publicIPAllowed = "Yes"
}
subnetData := map[string]string{
"ID": subnet.ID,
"Name": subnet.Name,
"Public IP": publicIPAllowed,
"CIDR": subnet.CIDR,
"DNS Label": subnet.DNSLabel,
"Subnet Domain": subnet.SubnetDomainName,
}
// Define ordered keys
orderedKeys := []string{
"ID", "Name", "Public IP", "CIDR", "DNS Label", "Subnet Domain",
}
// Create the colored title using components from the app context
title := util.FormatColoredTitle(appCtx, subnet.Name)
// Call the printer method to render the key-value from the app context.
p.PrintKeyValues(title, subnetData, orderedKeys)
}
util.LogPaginationInfo(nil, appCtx)
return nil
}
package subnet
import (
"context"
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"github.com/cnopslabs/ocloud/internal/oci"
"github.com/cnopslabs/ocloud/internal/services/util"
"github.com/oracle/oci-go-sdk/v65/core"
"strings"
)
// NewService creates and initializes a new Service instance using the provided application context.
// It returns the created Service or an error if initialization fails.
func NewService(appCtx *app.ApplicationContext) (*Service, error) {
cfg := appCtx.Provider
nc, err := oci.NewNetworkClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create network client: %w", err)
}
return &Service{
networkClient: nc,
logger: appCtx.Logger,
compartmentID: appCtx.CompartmentID,
}, nil
}
// List retrieves a paginated list of subnets based on the specified limit and page number within a compartment.
// It returns the subnet slice, the total count of subnets, the next page token, and an error if any occurs.
func (s *Service) List(ctx context.Context, limit int, pageNum int) ([]Subnet, int, string, error) {
var subnets []Subnet
var nextPageToken string
var totalCount int
// Prepare the base request
// Create a request with limited parameters to fetch only the required page
request := core.ListSubnetsRequest{
CompartmentId: &s.compartmentID,
}
// Add limit parameters
if limit > 0 {
request.Limit = &limit
logger.LogWithLevel(s.logger, 3, "Setting limit parameter", "limit", limit)
}
// If pageNum > 1, we need to fetch the appropriate page token
if pageNum > 1 && limit > 0 {
logger.LogWithLevel(s.logger, 3, "Calculating page token for page", "pageNum", pageNum)
// paginate through results; stop when OpcNextPage is nil
page := ""
currentPage := 1
for currentPage < pageNum {
// Fetch page token, not actual data
// Use limit to ensure consistent pagination
tokenRequest := core.ListSubnetsRequest{
CompartmentId: &s.compartmentID,
Page: &page,
}
if limit > 0 {
tokenRequest.Limit = &limit
}
resp, err := s.networkClient.ListSubnets(ctx, tokenRequest)
if err != nil {
return nil, 0, "", fmt.Errorf("fetching page token: %w", err)
}
// If there's no next page, we've reached the end
if resp.OpcNextPage == nil {
logger.LogWithLevel(s.logger, 3, "Reached end of data while calculating page token",
"currentPage", currentPage, "targetPage", pageNum)
// Return an empty result since the requested page is beyond available data
return []Subnet{}, 0, "", nil
}
// Move to the next page
page = *resp.OpcNextPage
currentPage++
}
// Set the page token for the actual request
request.Page = &page
logger.LogWithLevel(s.logger, 1, "Using page token for page", "pageNum", pageNum, "token", page)
}
// Fetch Subnets for the request
resp, err := s.networkClient.ListSubnets(ctx, request)
if err != nil {
return nil, 0, "", fmt.Errorf("listing subnets: %w", err)
}
// Set the total count to the number of subnets returned
// If we have a next page, this is an estimate
totalCount = len(resp.Items)
// If we have a next page, we know there are more subnets
if resp.OpcNextPage != nil {
// Estimate total count based on my current page and items per rage
totalCount = pageNum*limit + limit
}
//Save the next page token if available
if resp.OpcNextPage != nil {
nextPageToken = *resp.OpcNextPage
logger.LogWithLevel(s.logger, 3, "Next page token", "token", nextPageToken)
}
//Process the subnets
for _, oc := range resp.Items {
subnets = append(subnets, mapToSubnets(oc))
}
// Calculate if there are more pages after the current page
hasNextPage := pageNum*limit < totalCount
logger.LogWithLevel(s.logger, 2, "Completed instance listing with pagination",
"returnedCount", len(subnets),
"totalCount", totalCount,
"page", pageNum,
"limit", limit,
"hasNextPage", hasNextPage)
return subnets, totalCount, nextPageToken, nil
}
// Find retrieves a slice of subnets whose attributes match the provided name pattern using fuzzy search.
// It fetches all subnets, builds an index, performs a fuzzy search, and returns matched subnets or an error.
func (s *Service) Find(ctx context.Context, namePattern string) ([]Subnet, error) {
logger.LogWithLevel(s.logger, 3, "finding subnet with bleve fuzzy search", "pattern", namePattern)
var allSubnets []Subnet
// 1. Fetch all subnets in the compartment
allSubnets, err := s.fetchAllSubnets(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch all subnets: %w", err)
}
// 2. Build index
index, err := util.BuildIndex(allSubnets, func(s Subnet) any {
return mapToIndexableSubnets(s)
})
if err != nil {
return nil, fmt.Errorf("failed to build index: %w", err)
}
// 3. Fuzzy search on multiple fields
fields := []string{"Name", "CIDR"}
matchedIdxs, err := util.FuzzySearchIndex(index, namePattern, fields)
if err != nil {
return nil, fmt.Errorf("failed to fuzzy search index: %w", err)
}
// Return marched subnets
var matchedSubnets []Subnet
for _, idx := range matchedIdxs {
if idx >= 0 && idx < len(allSubnets) {
matchedSubnets = append(matchedSubnets, allSubnets[idx])
}
}
logger.LogWithLevel(s.logger, 2, "found subnet", "count", len(matchedSubnets))
return matchedSubnets, nil
}
// fetchAllSubnets retrieves all subnets within the specified compartment using pagination and returns them as a slice.
func (s *Service) fetchAllSubnets(ctx context.Context) ([]Subnet, error) {
var allSubnets []Subnet
page := ""
for {
resp, err := s.networkClient.ListSubnets(ctx, core.ListSubnetsRequest{
CompartmentId: &s.compartmentID,
Page: &page,
})
if err != nil {
return nil, fmt.Errorf("failed to list subnets: %w", err)
}
for _, s := range resp.Items {
allSubnets = append(allSubnets, mapToSubnets(s))
}
if resp.OpcNextPage == nil {
break
}
page = *resp.OpcNextPage
}
return allSubnets, nil
}
// mapToSubnets maps a core.Subnet object to a Subnet object while extracting and transforming its relevant fields.
func mapToSubnets(s core.Subnet) Subnet {
return Subnet{
Name: *s.DisplayName,
ID: *s.Id,
CIDR: *s.CidrBlock,
VcnID: *s.VcnId,
RouteTableID: *s.RouteTableId,
SecurityListID: s.SecurityListIds,
DhcpOptionsID: *s.DhcpOptionsId,
ProhibitPublicIPOnVnic: *s.ProhibitPublicIpOnVnic,
ProhibitInternetIngress: *s.ProhibitInternetIngress,
ProhibitInternetEgress: *s.ProhibitInternetIngress,
DNSLabel: *s.DnsLabel,
SubnetDomainName: *s.SubnetDomainName,
}
}
// mapToIndexableSubnets converts a Subnet object into an IndexableSubnet by transforming its attributes as needed for indexing.
func mapToIndexableSubnets(s Subnet) any {
return IndexableSubnet{
Name: strings.ToLower(s.Name),
CIDR: s.CIDR,
}
}
package util
import (
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/logger"
"io"
)
// LogPaginationInfo logs pagination information if available and prints it to the output.
func LogPaginationInfo(pagination *PaginationInfo, appCtx *app.ApplicationContext) {
// Log pagination information if available
if pagination != nil {
// Determine if there's a next page
hasNextPage := pagination.NextPageToken != ""
// Log pagination information at the INFO level
appCtx.Logger.Info("--- Pagination Information ---",
"page", pagination.CurrentPage,
"records", fmt.Sprintf("%d", pagination.TotalCount),
"limit", pagination.Limit,
"nextPage", hasNextPage)
// Print pagination information to the output if stdout is available
if appCtx.Stdout != nil {
fmt.Fprintf(appCtx.Stdout, "Page %d | Total: %d\n", pagination.CurrentPage, pagination.TotalCount)
}
// Add debug logs for navigation hints
if pagination.CurrentPage > 1 {
logger.LogWithLevel(appCtx.Logger, 2, "Pagination navigation",
"action", "previous page",
"page", pagination.CurrentPage-1,
"limit", pagination.Limit)
}
// Check if there are more pages after the current page
// The most direct way to determine if there are more pages is to check if there's a next page token
if pagination.NextPageToken != "" {
logger.LogWithLevel(appCtx.Logger, 2, "Pagination navigation",
"action", "next page",
"page", pagination.CurrentPage+1,
"limit", pagination.Limit)
}
}
}
// AdjustPaginationInfo adjusts the pagination information to ensure that the total count
// is correctly displayed. It calculates the total records displayed so far and updates
// the TotalCount field of the pagination object to match this value.
func AdjustPaginationInfo(pagination *PaginationInfo) {
// Calculate the total records displayed so far
totalRecordsDisplayed := pagination.CurrentPage * pagination.Limit
if totalRecordsDisplayed > pagination.TotalCount {
totalRecordsDisplayed = pagination.TotalCount
}
// Update the total count to match the total records displayed so far
// This ensures that on page 1 we show 20, on page 2 we show 40, on page 3 we show 60, etc.
pagination.TotalCount = totalRecordsDisplayed
}
// ValidateAndReportEmpty handles the case when a generic list is empty and provides pagination hints.
func ValidateAndReportEmpty[T any](items []T, pagination *PaginationInfo, out io.Writer) bool {
if len(items) > 0 {
return false
}
fmt.Fprintf(out, "No Items found.\n")
if pagination != nil && pagination.TotalCount > 0 {
fmt.Fprintf(out, "Page %d is empty. Total records: %d\n", pagination.CurrentPage, pagination.TotalCount)
if pagination.CurrentPage > 1 {
fmt.Fprintf(out, "Try a lower page number (e.g., --page %d)\n", pagination.CurrentPage-1)
}
}
return true
}
package util
import (
"fmt"
"github.com/cnopslabs/ocloud/internal/app"
"github.com/cnopslabs/ocloud/internal/printer"
"github.com/jedib0t/go-pretty/v6/text"
)
// MarshalDataToJSONResponse now accepts a printer and returns an error.
func MarshalDataToJSONResponse[T any](p *printer.Printer, items []T, pagination *PaginationInfo) error {
response := JSONResponse[T]{
Items: items,
Pagination: pagination,
}
return p.MarshalToJSON(response)
}
// FormatColoredTitle builds a colorized title string with tenancy, compartment, and cluster.
func FormatColoredTitle(appCtx *app.ApplicationContext, name string) string {
// Create the colored title using components from the app context.
coloredTenancy := text.Colors{text.FgMagenta}.Sprint(appCtx.TenancyName)
coloredCompartment := text.Colors{text.FgCyan}.Sprint(appCtx.CompartmentName)
coloredName := text.Colors{text.FgBlue}.Sprint(name)
title := fmt.Sprintf("%s: %s: %s", coloredTenancy, coloredCompartment, coloredName)
return title
}
package util
import (
"fmt"
"github.com/blevesearch/bleve/v2"
bleveQuery "github.com/blevesearch/bleve/v2/search/query"
"strconv"
"strings"
)
// BuildIndex creates an in-memory Bleve index from a slice of items using a provided mapping function.
// The mapping function converts each item into an indexable structure for searching.
// Returns the built index or an error if any indexing operation fails.
func BuildIndex[T any](items []T, mapToIndexable func(T) any) (bleve.Index, error) {
indexMapping := bleve.NewIndexMapping()
index, err := bleve.NewMemOnly(indexMapping)
if err != nil {
return nil, fmt.Errorf("creating index: %w", err)
}
for i, item := range items {
err := index.Index(fmt.Sprintf("%d", i), mapToIndexable(item))
if err != nil {
return nil, fmt.Errorf("indexing failed at %d: %w", i, err)
}
}
return index, nil
}
// FuzzySearchIndex performs a fuzzy search on a Bleve index for a given pattern across specified fields.
// It combines fuzzy, prefix, and wildcard queries, limits the results, and returns matched indices or an error.
func FuzzySearchIndex(index bleve.Index, pattern string, fields []string) ([]int, error) {
var limit = 1000
var queries []bleveQuery.Query
for _, field := range fields {
// Fuzzy match (Levenshtein distance)
fq := bleve.NewFuzzyQuery(pattern)
fq.SetField(field)
fq.SetFuzziness(2)
queries = append(queries, fq)
// Prefix match (useful for dev1, splunkdev1, etc.)
pq := bleve.NewPrefixQuery(pattern)
pq.SetField(field)
queries = append(queries, pq)
// Wildcard match (matches anywhere in token)
wq := bleve.NewWildcardQuery("*" + pattern + "*")
wq.SetField(field)
queries = append(queries, wq)
}
// OR all queries together
combinedQuery := bleve.NewDisjunctionQuery(queries...)
search := bleve.NewSearchRequestOptions(combinedQuery, limit, 0, false)
result, err := index.Search(search)
if err != nil {
return nil, err
}
var hits []int
for _, hit := range result.Hits {
idx, err := strconv.Atoi(hit.ID)
if err != nil {
continue
}
hits = append(hits, idx)
}
return hits, nil
}
// FlattenTags flattens freeform and defined tags into a single string with a specific format suitable for indexing.
// Freeform tags are processed as key:value pairs, while defined tags include namespace, key, and value.
// Returns the flattened string or an empty string if no valid tags are found.
func FlattenTags(freeform map[string]string, defined map[string]map[string]interface{}) (string, error) {
var parts []string
// Handle Freeform Tags
if freeform != nil {
for k, v := range freeform {
if k == "" || v == "" {
continue // skip empty keys/values
}
parts = append(parts, fmt.Sprintf("%s:%s", strings.ToLower(k), strings.ToLower(v)))
}
}
// Handle Defined Tags
if defined != nil {
for ns, kv := range defined {
if ns == "" || kv == nil {
continue
}
for k, v := range kv {
if k == "" || v == nil {
continue
}
// You can restrict this to specific types if desired (e.g., string only)
// Convert to string safely
var valueStr string
switch val := v.(type) {
case string:
valueStr = val
default:
// fallback to fmt.Sprintf
valueStr = fmt.Sprintf("%v", val)
}
parts = append(parts, fmt.Sprintf("%s.%s:%s", strings.ToLower(ns), strings.ToLower(k), valueStr))
}
}
}
if len(parts) == 0 {
return "", nil // Return an empty string without error when no tags are found
}
return strings.Join(parts, " "), nil
}
// ExtractTagValues extracts only the values from freeform and defined tags into a single space-separated string.
// This is useful for making tag values directly searchable without requiring the key prefix.
// Returns the extracted values string or an empty string if no valid tag values are found.
func ExtractTagValues(freeform map[string]string, defined map[string]map[string]interface{}) (string, error) {
var values []string
// Extract values from Freeform Tags
if freeform != nil {
for _, v := range freeform {
if v == "" {
continue // skip empty values
}
values = append(values, strings.ToLower(v))
}
}
// Extract values from Defined Tags
if defined != nil {
for _, kv := range defined {
if kv == nil {
continue
}
for _, v := range kv {
if v == nil {
continue
}
// Convert to string safely
var valueStr string
switch val := v.(type) {
case string:
valueStr = val
default:
// fallback to fmt.Sprintf
valueStr = fmt.Sprintf("%v", val)
}
if valueStr != "" {
values = append(values, strings.ToLower(valueStr))
}
}
}
}
if len(values) == 0 {
return "", nil // Return an empty string without error when no tag values are found
}
return strings.Join(values, " "), nil
}
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/cnopslabs/ocloud/cmd"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := cmd.Execute(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}